@jxsuite/studio 0.33.0 → 0.35.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 (106) hide show
  1. package/dist/iframe-entry.js +6238 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +30972 -17222
  4. package/dist/studio.js.map +328 -83
  5. package/package.json +9 -9
  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 +5 -1
  41. package/src/new-project/design-fields.ts +260 -0
  42. package/src/new-project/import-tab.ts +322 -0
  43. package/src/new-project/new-project-modal.ts +472 -89
  44. package/src/new-project/templates.ts +61 -0
  45. package/src/packages/ensure-deps.ts +6 -0
  46. package/src/packages/pull-package-sync.ts +339 -0
  47. package/src/page-params.ts +383 -0
  48. package/src/panels/ai-chat/attached-context.ts +49 -0
  49. package/src/panels/ai-chat/chat-markdown.ts +38 -0
  50. package/src/panels/ai-chat/chat-view.ts +250 -0
  51. package/src/panels/ai-chat/composer.ts +315 -0
  52. package/src/panels/ai-chat/sessions-view.ts +98 -0
  53. package/src/panels/ai-panel.ts +189 -457
  54. package/src/panels/block-action-bar.ts +296 -138
  55. package/src/panels/canvas-dnd-bridge.ts +397 -0
  56. package/src/panels/component-preview.ts +56 -0
  57. package/src/panels/dnd.ts +41 -17
  58. package/src/panels/drag-ghost.ts +62 -0
  59. package/src/panels/editors.ts +1 -1
  60. package/src/panels/git-panel.ts +16 -1
  61. package/src/panels/overlays.ts +10 -125
  62. package/src/panels/properties-panel.ts +210 -0
  63. package/src/panels/right-panel.ts +21 -7
  64. package/src/panels/signals-panel.ts +136 -22
  65. package/src/panels/stylebook-doc.ts +373 -0
  66. package/src/panels/stylebook-panel.ts +46 -689
  67. package/src/panels/tab-bar.ts +159 -13
  68. package/src/panels/toolbar.ts +3 -2
  69. package/src/platforms/devserver.ts +45 -1
  70. package/src/services/agent-seed.ts +91 -0
  71. package/src/services/ai-models.ts +64 -0
  72. package/src/services/ai-session-store.ts +250 -0
  73. package/src/services/ai-settings.ts +5 -0
  74. package/src/services/automation.ts +140 -0
  75. package/src/services/document-assistant.ts +98 -38
  76. package/src/services/import-client.ts +134 -0
  77. package/src/services/monaco-setup.ts +12 -0
  78. package/src/services/render-critic.ts +9 -9
  79. package/src/services/settings-store.ts +93 -0
  80. package/src/settings/css-vars-editor.ts +2 -2
  81. package/src/store.ts +4 -62
  82. package/src/studio.ts +115 -40
  83. package/src/tabs/doc-op-apply.ts +89 -0
  84. package/src/tabs/patch-ops.ts +6 -2
  85. package/src/tabs/tab.ts +23 -4
  86. package/src/tabs/transact.ts +2 -74
  87. package/src/types.ts +98 -18
  88. package/src/ui/ai-credentials-form.ts +205 -0
  89. package/src/ui/jx-theme.ts +63 -0
  90. package/src/ui/media-picker.ts +6 -4
  91. package/src/ui/spectrum.ts +13 -0
  92. package/src/utils/canvas-media.ts +0 -137
  93. package/src/utils/edit-display.ts +23 -3
  94. package/src/utils/geometry.ts +43 -0
  95. package/src/utils/link-target.ts +93 -0
  96. package/src/utils/strip-events.ts +54 -0
  97. package/src/view.ts +0 -23
  98. package/src/workspace/workspace.ts +14 -1
  99. package/src/canvas/canvas-subtree-render.ts +0 -113
  100. package/src/editor/component-inline-edit.ts +0 -349
  101. package/src/editor/content-inline-edit.ts +0 -207
  102. package/src/editor/insertion-helper.ts +0 -308
  103. package/src/panels/canvas-dnd.ts +0 -329
  104. package/src/panels/panel-events.ts +0 -306
  105. package/src/panels/preview-render.ts +0 -132
  106. package/src/panels/pseudo-preview.ts +0 -75
@@ -6,17 +6,9 @@
6
6
  * mapping ($map remapping), site-level style injection, and $head element injection.
7
7
  */
8
8
 
9
- import { elToPath, elToScope, projectState, stripEventHandlers } from "../store";
10
- import { errorMessage } from "@jxsuite/schema/parse";
9
+ import { projectState, stripEventHandlers } from "../store";
11
10
  import { activeTab } from "../workspace/workspace";
12
- import { view } from "../view";
13
- import { effectScope, toRaw } from "../reactivity";
14
- import {
15
- buildScope,
16
- defineElement,
17
- renderNode as runtimeRenderNode,
18
- setSkipServerFunctions,
19
- } from "@jxsuite/runtime";
11
+ import { toRaw } from "../reactivity";
20
12
  import {
21
13
  distributePageIntoLayout,
22
14
  getEffectiveElements,
@@ -26,38 +18,21 @@ import {
26
18
  getEffectiveMedia,
27
19
  resolveLayoutDoc,
28
20
  } from "../site-context";
21
+ import { canvasBaseOrigin } from "./canvas-origin";
29
22
  import { componentRegistry, computeRelativePath } from "../files/components";
30
23
  import { prepareForEditMode } from "../utils/edit-display";
31
- import { getActiveElement } from "../editor/inline-edit";
32
- import { buildNestedSiteCSS } from "./nested-site-style";
24
+ import {
25
+ paramBoundStateKeys,
26
+ resolveParamBoundState,
27
+ substitutePreviewParams,
28
+ } from "../page-params";
33
29
 
34
- import type { JxDocument, JxElement, JxMutableNode } from "@jxsuite/schema/types";
30
+ import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
35
31
  import type { ComponentEntry } from "../files/components.js";
36
- import type { CanvasPanel } from "../types";
37
- import type { EffectScope } from "../reactivity";
38
-
39
- export { buildNestedSiteCSS } from "./nested-site-style";
40
-
41
- /** @param {Event} e */
42
- function _preventNav(e: Event) {
43
- if ((e.target as HTMLElement).closest("a[href]")) {
44
- e.preventDefault();
45
- }
46
- }
47
-
48
- /** Canvas elements that already have the delegated nav guard listener. */
49
- const _navGuarded = new WeakSet();
32
+ import type { WireMapperCtx } from "./iframe-protocol";
50
33
 
51
34
  let _ctx: { getCanvasMode: () => string } | null = null;
52
35
 
53
- /** Set of DOM elements that originated from the layout (not page content). */
54
- export const layoutElements = new WeakSet();
55
-
56
- /** Cache of element HREFs that failed to load — prevents infinite retry loops. */
57
- const _failedElements = new Set();
58
-
59
- let _failedElementsDocPath: string | null = null;
60
-
61
36
  /**
62
37
  * Walk the merged document tree to find where page children were distributed into the layout slot.
63
38
  * Returns the `prefix` path to the container whose children hold the page content (first container
@@ -98,9 +73,6 @@ function findPageContentPrefix(
98
73
  return null;
99
74
  }
100
75
 
101
- /** The path of the currently active layout file, or null. */
102
- export let activeLayoutPath = null as string | null;
103
-
104
76
  /**
105
77
  * Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
106
78
  * rendered DOM elements came from the layout vs page content.
@@ -138,156 +110,43 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
138
110
  _ctx = ctx;
139
111
  }
140
112
 
141
- /** Context needed to map runtime render paths back to document paths. */
142
- export interface PathMapperCtx {
143
- canvasMode: string;
144
- layoutWrapped: boolean;
145
- pageContentPrefix: (string | number)[] | null;
146
- /** Index of the first page-content child within the slot container (leading layout siblings). */
147
- pageContentOffset?: number;
148
- /** Document paths of every mapped-array node, used to remap edit-mode repeater perimeters. */
149
- arrayPaths: Set<string>;
150
- }
151
-
152
113
  /**
153
- * Build the onNodeCreated callback that records each rendered element's document path in elToPath.
154
- * Handles layout-prefix stripping and repeater-perimeter/template remapping. Shared by full panel
155
- * renders and isolated subtree re-renders so path bookkeeping stays consistent.
156
- */
157
- export function makePathMapper(ctx: PathMapperCtx) {
158
- const { canvasMode, layoutWrapped, pageContentPrefix, pageContentOffset, arrayPaths } = ctx;
159
- return function onNodeCreated(
160
- created: Node,
161
- path: (string | number)[],
162
- def: unknown,
163
- state?: Record<string, unknown>,
164
- ) {
165
- if (!(created instanceof HTMLElement)) {
166
- return;
167
- }
168
- if (state) {
169
- elToScope.set(created, state);
170
- }
171
- // Track layout-originated elements — don't store in elToPath to avoid
172
- // Path collisions with remapped page content paths
173
- if (layoutWrapped && typeof def === "object" && (def as JxMutableNode)?.$__layout) {
174
- layoutElements.add(created);
175
- created.dataset.jxLayout = "";
176
- return;
177
- }
178
-
179
- // Remap layout-wrapped paths: strip the layout prefix so paths are
180
- // Relative to the original page document (which is what S.document holds)
181
- let mappedPath = path;
182
- if (layoutWrapped && pageContentPrefix) {
183
- const pfx = pageContentPrefix;
184
- if (
185
- path.length >= pfx.length &&
186
- pfx.every((seg: string | number, i: number) => path[i] === seg)
187
- ) {
188
- // Page children render at container indices [offset, offset+1, …] when the layout places
189
- // Sibling nodes before the <slot>. Subtract the offset so they map back to the page
190
- // Document's 0-based child indices (what flattenTree / the layers panel use).
191
- const rest = path.slice(pfx.length);
192
- const [containerIdx] = rest;
193
- mappedPath =
194
- typeof containerIdx === "number"
195
- ? ["children", containerIdx - (pageContentOffset ?? 0), ...rest.slice(1)]
196
- : ["children", ...rest];
197
- }
198
- }
199
-
200
- // Remap repeater perimeters: prepareForEditMode renders each mapped-array node as a
201
- // `<div class="repeater-perimeter">` at the array's own child index, with the map template as
202
- // Its single child[0]. The perimeter's render path already equals the array's document path,
203
- // So only the template hop needs collapsing: for any array document path P, a render path of
204
- // `[...P, "children", 0, ...rest]` maps to `[...P, "map", ...rest]`. Looping handles nested
205
- // Repeaters (an array whose template contains another array).
206
- if ((canvasMode === "design" || canvasMode === "edit") && arrayPaths.size > 0) {
207
- let changed = true;
208
- while (changed) {
209
- changed = false;
210
- for (let i = 1; i < mappedPath.length - 1; i++) {
211
- if (
212
- mappedPath[i] === "children" &&
213
- mappedPath[i + 1] === 0 &&
214
- arrayPaths.has(mappedPath.slice(0, i).join("/"))
215
- ) {
216
- mappedPath = [...mappedPath.slice(0, i), "map", ...mappedPath.slice(i + 2)];
217
- changed = true;
218
- break;
219
- }
220
- }
221
- }
222
- }
223
- elToPath.set(created, mappedPath);
224
- };
225
- }
226
-
227
- /**
228
- * Render a Jx document into a canvas element using the real runtime. Populates elToPath for each
229
- * created element via onNodeCreated callback. Returns the live state scope on success, null on
230
- * failure.
231
- *
232
- * @param {number} gen - Render generation for staleness detection
233
- * @param {JxMutableNode} doc
234
- * @param {HTMLElement} canvasEl
235
- * @param {CanvasPanel | null} [panel] - Panel to persist render context on (for surgical patches)
114
+ * Resolve a document into the form the iframe canvas renders: layout-distributed, edit-transformed,
115
+ * with components/imports/$media/$head merged in, plus the path-mapper context and site style. This
116
+ * is the "parent resolves, iframe renders" split the realm-specific work (defineElement,
117
+ * injecting $head/site-style into the DOM, buildScope/renderNode) happens inside the iframe from
118
+ * this result.
236
119
  */
237
- export async function renderCanvasLive(
238
- gen: number,
239
- doc: JxMutableNode,
240
- canvasEl: HTMLElement,
241
- panel: CanvasPanel | null = null,
242
- ) {
120
+ export async function resolveCanvasDocument(doc: JxMutableNode): Promise<{
121
+ renderDoc: JxMutableNode;
122
+ docBase: string | undefined;
123
+ mapperCtx: WireMapperCtx;
124
+ siteStyle: Record<string, unknown> | null;
125
+ }> {
243
126
  const tab = activeTab.value;
244
- const S = {
245
- document: tab?.doc.document,
246
- documentPath: tab?.documentPath,
247
- mode: tab?.doc.mode,
248
- };
127
+ const S = { documentPath: tab?.documentPath, mode: tab?.doc.mode };
249
128
  const canvasMode = _ctx!.getCanvasMode();
250
129
 
251
- if (S.documentPath !== _failedElementsDocPath) {
252
- _failedElements.clear();
253
- _failedElementsDocPath = S.documentPath ?? null;
254
- }
255
-
256
- // Suppress server function resolution in non-preview modes to avoid
257
- // Failed proxy calls and infinite reactive retries (also covers
258
- // Async custom element connectedCallbacks that run after this function returns)
259
- setSkipServerFunctions(canvasMode !== "preview");
260
-
261
130
  let renderDoc =
262
131
  canvasMode === "preview"
263
132
  ? structuredClone(toRaw(doc))
264
133
  : prepareForEditMode(stripEventHandlers(doc));
265
134
 
266
- // ─── Layout wrapping ────────────────────────────────────────────────────
267
- // For page documents, resolve the layout and wrap content in the layout shell.
268
- // Layout-originated nodes are marked with $__layout so we can distinguish them.
269
135
  let layoutWrapped = false;
270
- activeLayoutPath = null;
271
-
272
136
  const isPage =
273
137
  S.documentPath &&
274
138
  projectState?.isSiteProject &&
275
139
  (S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
276
-
277
- /** @type {(string | number)[] | null} Path prefix in merged doc where page children live */
278
- let pageContentPrefix = null;
279
- /** Index of the first page child within the slot container (offset by leading layout siblings). */
140
+ let pageContentPrefix: (string | number)[] | null = null;
280
141
  let pageContentOffset = 0;
281
142
 
282
- if (isPage) {
143
+ // Layout wrapping obeys the tab-bar's "show layout elements" toggle (default on); with it off,
144
+ // The page renders alone — the unwrapped path is identical to a non-layout page.
145
+ if (isPage && tab?.session.ui.showLayout !== false) {
283
146
  const layoutPath = getEffectiveLayoutPath(doc.$layout);
284
147
  if (layoutPath) {
285
148
  const layoutDoc = (await resolveLayoutDoc(layoutPath)) as JxMutableNode | null;
286
149
  if (layoutDoc) {
287
- if (gen !== view.renderGeneration) {
288
- return null;
289
- }
290
- activeLayoutPath = layoutPath.replace(/^\.\//, "");
291
150
  markLayoutNodes(layoutDoc);
292
151
  const pageForSlots = canvasMode === "preview" ? structuredClone(toRaw(doc)) : renderDoc;
293
152
  const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
@@ -301,16 +160,34 @@ export async function renderCanvasLive(
301
160
  }
302
161
  }
303
162
 
304
- // In edit mode, collect the document path of every mapped-array node so the path mapper can
305
- // Remap their edit-mode perimeters (and templates) back to document paths.
163
+ const root = projectState?.projectRoot || "";
164
+ const docPrefix = root ? `${root}/` : "";
165
+ const docBase = S.documentPath
166
+ ? `${canvasBaseOrigin()}/${docPrefix}${S.documentPath}`
167
+ : undefined;
168
+
169
+ // Substitute chosen dynamic route params ({$ref: "#/$params/x"} → literal), inject state.$page,
170
+ // And bake the substituted class-prototype state entries via the backend resolver — in every
171
+ // Mode, so ContentEntry state is real data for preview templates AND the design/edit data
172
+ // Explorer. Pure rebuild: renderDoc shares node references with the tab's source document
173
+ // (posted as shadowDoc), whose $refs must survive for editing/serialization.
174
+ if (isPage && tab) {
175
+ const { previewParams } = tab.session.ui;
176
+ if (previewParams && Object.keys(previewParams).length > 0) {
177
+ const boundKeys = paramBoundStateKeys(
178
+ (doc as { state?: Record<string, unknown> }).state ?? null,
179
+ );
180
+ renderDoc = substitutePreviewParams(renderDoc, previewParams, S.documentPath);
181
+ await resolveParamBoundState(renderDoc, boundKeys, docBase);
182
+ }
183
+ }
184
+
306
185
  const arrayPaths = new Set<string>();
307
- let renderScope: EffectScope | null = null;
308
186
  if (canvasMode === "design" || canvasMode === "edit") {
309
187
  (function findArrayPaths(node: JxMutableNode, path: (string | number)[]) {
310
188
  if (!node || typeof node !== "object") {
311
189
  return;
312
190
  }
313
- // The node itself is a mapped array (reached as a member or whole-children slot).
314
191
  if ((node as unknown as { $prototype?: string }).$prototype === "Array") {
315
192
  arrayPaths.add(path.join("/"));
316
193
  const mapDef = (node as JxMutableNode).map;
@@ -332,7 +209,6 @@ export async function renderCanvasLive(
332
209
  typeof node.children === "object" &&
333
210
  (node.children as unknown as { $prototype?: string }).$prototype === "Array"
334
211
  ) {
335
- // Legacy whole-children repeater.
336
212
  findArrayPaths(node.children as JxMutableNode, [...path, "children"]);
337
213
  }
338
214
  if (node.$switch && node.cases) {
@@ -343,281 +219,72 @@ export async function renderCanvasLive(
343
219
  })(doc, []);
344
220
  }
345
221
 
346
- try {
347
- const root = projectState?.projectRoot || "";
348
- const docPrefix = root ? `${root}/` : "";
349
- const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
350
-
351
- // Register custom elements so the runtime can render them
352
- const effectiveElements = getEffectiveElements(renderDoc.$elements as (JxElement | string)[]);
353
-
354
- // In content mode (markdown) or when a layout is applied, auto-discover components
355
- // For custom elements that have no explicit $elements registration.
356
- if ((S.mode === "content" || layoutWrapped) && componentRegistry.length > 0) {
357
- const existingRefs = new Set(
358
- effectiveElements.map((e: JxElement | string) => (typeof e === "string" ? e : e?.$ref)),
359
- );
360
- /** @param {JxMutableNode} node */
361
- const collectTags = (node: JxMutableNode) => {
362
- const tags = new Set<string>();
363
- if (!node || typeof node !== "object") {
364
- return tags;
365
- }
366
- if (node.tagName) {
367
- tags.add(node.tagName);
368
- }
369
- if (Array.isArray(node.children)) {
370
- for (const child of node.children) {
371
- if (typeof child === "string") {
372
- continue;
373
- }
374
- for (const t of collectTags(child)) {
375
- tags.add(t);
376
- }
377
- }
378
- }
222
+ // Component auto-discovery (content mode or layout) — mirrors the legacy render path.
223
+ const effectiveElements = getEffectiveElements(renderDoc.$elements as (JxElement | string)[]);
224
+ if ((S.mode === "content" || layoutWrapped) && componentRegistry.length > 0) {
225
+ const existingRefs = new Set(
226
+ effectiveElements.map((e: JxElement | string) => (typeof e === "string" ? e : e?.$ref)),
227
+ );
228
+ const collectTags = (node: JxMutableNode): Set<string> => {
229
+ const tags = new Set<string>();
230
+ if (!node || typeof node !== "object") {
379
231
  return tags;
380
- };
381
- for (const tag of collectTags(renderDoc)) {
382
- const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
383
- if (comp && comp.source !== "npm" && comp.path) {
384
- const relPath = computeRelativePath(S.documentPath ?? null, comp.path);
385
- if (!existingRefs.has(relPath)) {
386
- effectiveElements.push({ $ref: relPath });
387
- existingRefs.add(relPath);
388
- }
389
- }
390
232
  }
391
- }
392
-
393
- if (effectiveElements.length > 0) {
394
- renderDoc.$elements = effectiveElements as (string | JxMutableNode | { $ref: string })[];
395
- for (const entry of effectiveElements) {
396
- if (typeof entry === "string") {
397
- try {
398
- const specifier =
399
- entry.startsWith("/") || entry.startsWith(".") ? entry : `/node_modules/${entry}`;
400
- await import(specifier);
401
- } catch (error) {
402
- console.warn("Studio: failed to import package", entry, error);
403
- }
404
- } else if (entry?.$ref) {
405
- let href;
406
- try {
407
- ({ href } = new URL(entry.$ref, docBase));
408
- } catch (error) {
409
- console.warn("Studio: invalid element URL", { docBase, ref: entry.$ref }, error);
410
- continue;
411
- }
412
- if (_failedElements.has(href)) {
413
- continue;
414
- }
415
- try {
416
- await defineElement(href);
417
- } catch (error) {
418
- _failedElements.add(href);
419
- console.warn("Studio: failed to register element", entry.$ref, error);
420
- }
421
- }
233
+ if (node.tagName) {
234
+ tags.add(node.tagName);
422
235
  }
423
- }
424
-
425
- // Bail out if a newer render started while we were importing elements
426
- if (gen !== view.renderGeneration) {
427
- return null;
428
- }
429
-
430
- // Inject site-level imports so buildScope can resolve $prototype names
431
- renderDoc.imports = getEffectiveImports(renderDoc.imports);
432
-
433
- // Apply project-level styles mirroring the compiler convention:
434
- // Viewport ≈ :root → CSS custom properties (they inherit down)
435
- // CanvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
436
- // This ensures project font-family, color, etc. override the
437
- // Content-mode fallback typography rules in the stylesheet.
438
- // In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
439
- const viewport = canvasEl.closest(".canvas-panel-viewport") as HTMLElement | null;
440
- const editSurface =
441
- canvasMode === "edit"
442
- ? (canvasEl.closest(".content-edit-canvas") as HTMLElement | null)
443
- : null;
444
- const siteStyle = projectState?.projectConfig?.style;
445
- if (viewport) {
446
- viewport.style.cssText = "";
447
- if (siteStyle && typeof siteStyle === "object") {
448
- for (const [k, v] of Object.entries(siteStyle)) {
449
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
450
- continue;
451
- }
452
- if (k.startsWith("--")) {
453
- viewport.style.setProperty(k, String(v));
454
- } else {
455
- (viewport.style as unknown as Record<string, string>)[k] = String(v);
236
+ if (Array.isArray(node.children)) {
237
+ for (const child of node.children) {
238
+ if (typeof child !== "string") {
239
+ for (const t of collectTags(child)) {
240
+ tags.add(t);
241
+ }
456
242
  }
457
243
  }
458
244
  }
459
- }
460
- if (editSurface && siteStyle && typeof siteStyle === "object") {
461
- for (const [k, v] of Object.entries(siteStyle)) {
462
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
463
- continue;
464
- }
465
- if (k.startsWith("--")) {
466
- editSurface.style.setProperty(k, String(v));
467
- } else {
468
- (editSurface.style as unknown as Record<string, string>)[k] = String(v);
469
- }
470
- }
471
- }
472
- if (siteStyle && typeof siteStyle === "object") {
473
- for (const [k, v] of Object.entries(siteStyle)) {
474
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
475
- continue;
476
- }
477
- if (!k.startsWith("--")) {
478
- (canvasEl.style as unknown as Record<string, string>)[k] = String(v);
479
- }
480
- }
481
- }
482
-
483
- // Generate a <style> tag for nested selector rules (e.g. table, thead, etc.)
484
- if (siteStyle && typeof siteStyle === "object") {
485
- const scopeAttr = `data-jx-site`;
486
- canvasEl.setAttribute(scopeAttr, "");
487
- const css = buildNestedSiteCSS(siteStyle, `[${scopeAttr}]`);
488
-
489
- if (css) {
490
- const existingStyleEl = document.querySelector("#jx-site-style");
491
- if (existingStyleEl) {
492
- existingStyleEl.remove();
493
- }
494
- const styleEl = document.createElement("style");
495
- styleEl.id = "jx-site-style";
496
- styleEl.textContent = css;
497
- document.head.append(styleEl);
498
- }
499
- }
500
-
501
- // Inject site-level $media so runtime can resolve media queries in styles
502
- renderDoc.$media = getEffectiveMedia(renderDoc.$media);
503
-
504
- // Inject $head elements (link/meta/script) into document.head
505
- const effectiveHead = getEffectiveHead(renderDoc.$head);
506
- if (effectiveHead.length > 0) {
507
- for (const entry of effectiveHead) {
508
- if (!entry?.tagName) {
509
- continue;
510
- }
511
- const tag = entry.tagName.toLowerCase();
512
- // Skip inline scripts — they can contain arbitrary JS/HTML that throws
513
- // When the browser tries to execute it in the studio context
514
- if (tag === "script" && !entry.attributes?.src) {
515
- continue;
516
- }
517
- const attrs = { ...entry.attributes };
518
- for (const key of ["href", "src"]) {
519
- const val = attrs[key];
520
- if (
521
- typeof val === "string" &&
522
- !val.startsWith("/") &&
523
- !val.startsWith(".") &&
524
- !val.startsWith("http")
525
- ) {
526
- attrs[key] = `/node_modules/${val}`;
527
- }
528
- }
529
- const selector = `${tag}${attrs.href ? `[href="${attrs.href}"]` : ""}${attrs.src ? `[src="${attrs.src}"]` : ""}`;
530
- if (selector !== tag && document.head.querySelector(selector)) {
531
- continue;
532
- }
533
- const el = document.createElement(tag);
534
- for (const [k, v] of Object.entries(attrs)) {
535
- el.setAttribute(k, String(v));
536
- }
537
- if (entry.textContent) {
538
- el.textContent = entry.textContent;
245
+ return tags;
246
+ };
247
+ for (const tag of collectTags(renderDoc)) {
248
+ const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
249
+ if (comp && comp.source !== "npm" && comp.path) {
250
+ const relPath = computeRelativePath(S.documentPath ?? null, comp.path);
251
+ if (!existingRefs.has(relPath)) {
252
+ effectiveElements.push({ $ref: relPath });
253
+ existingRefs.add(relPath);
539
254
  }
540
- document.head.append(el);
541
255
  }
542
256
  }
257
+ }
258
+ if (effectiveElements.length > 0) {
259
+ renderDoc.$elements = effectiveElements as (string | JxMutableNode | { $ref: string })[];
260
+ }
261
+ renderDoc.imports = getEffectiveImports(renderDoc.imports);
262
+ // The effective-media/head getters return the merged value or undefined. Optional properties
263
+ // Cannot be assigned undefined under exactOptionalPropertyTypes, so clear them by deleting the key
264
+ // When the getter yields nothing.
265
+ const media = getEffectiveMedia(renderDoc.$media);
266
+ if (media === undefined) {
267
+ delete renderDoc.$media;
268
+ } else {
269
+ renderDoc.$media = media as NonNullable<JxMutableNode["$media"]>;
270
+ }
271
+ const head = getEffectiveHead(renderDoc.$head);
272
+ if (head === undefined) {
273
+ delete renderDoc.$head;
274
+ } else {
275
+ renderDoc.$head = head as NonNullable<JxMutableNode["$head"]>;
276
+ }
543
277
 
544
- const $defs = await buildScope(renderDoc as JxDocument, {}, docBase);
545
- // Bail out if a newer render started while buildScope was running
546
- if (gen !== view.renderGeneration) {
547
- return null;
548
- }
549
- const pathMapper = makePathMapper({
550
- arrayPaths,
278
+ return {
279
+ docBase,
280
+ mapperCtx: {
281
+ arrayPaths: [...arrayPaths],
551
282
  canvasMode,
552
283
  layoutWrapped,
553
284
  pageContentOffset,
554
285
  pageContentPrefix,
555
- });
556
- // Render inside a detached effect scope so the tree's reactive effects (template bindings,
557
- // $map/$switch containers) can be disposed when the panel is rebuilt, instead of leaking.
558
- renderScope = effectScope(true);
559
- const el = /** @type {HTMLElement} */ renderScope.run(() =>
560
- runtimeRenderNode(renderDoc, $defs, {
561
- _path: [],
562
- onNodeCreated: pathMapper,
563
- }),
564
- )!;
565
- if ((canvasMode === "design" || canvasMode === "edit") && el instanceof HTMLElement) {
566
- // Disable pointer events on all rendered elements for edit mode
567
- el.style.pointerEvents = "none";
568
- for (const child of el.querySelectorAll("*")) {
569
- (child as HTMLElement).style.pointerEvents = "none";
570
- }
571
- }
572
- // Clear and append atomically — ensures the canvas is never left empty if a
573
- // Newer render starts and this one would have bailed after clearing.
574
- canvasEl.innerHTML = "";
575
- if (S.mode === "content") {
576
- canvasEl.dataset.contentMode = "";
577
- } else {
578
- delete canvasEl.dataset.contentMode;
579
- }
580
-
581
- canvasEl.append(el);
582
-
583
- // Delegated click handler prevents link navigation in all canvas modes.
584
- // Attached once per canvasEl (survives reactive re-renders that replace children).
585
- if (!_navGuarded.has(canvasEl)) {
586
- canvasEl.addEventListener("click", _preventNav);
587
- _navGuarded.add(canvasEl);
588
- }
589
-
590
- if (canvasMode === "design" || canvasMode === "edit") {
591
- requestAnimationFrame(() => {
592
- const editingEl = getActiveElement();
593
- for (const child of canvasEl.querySelectorAll("*")) {
594
- if (view.componentInlineEdit && child === view.componentInlineEdit.el) {
595
- continue;
596
- }
597
- if (editingEl && child === editingEl) {
598
- continue;
599
- }
600
- (child as HTMLElement).style.pointerEvents = "none";
601
- }
602
- });
603
- }
604
- if (panel) {
605
- panel.renderScope?.stop();
606
- panel.renderScope = renderScope;
607
- panel.liveCtx = {
608
- arrayPaths,
609
- canvasMode,
610
- layoutWrapped,
611
- pageContentOffset,
612
- pageContentPrefix,
613
- pathMapper,
614
- scope: $defs as Record<string, unknown>,
615
- };
616
- }
617
- return $defs;
618
- } catch (error) {
619
- renderScope?.stop();
620
- console.warn("renderCanvasLive failed:", errorMessage(error), error);
621
- return null;
622
- }
286
+ },
287
+ renderDoc,
288
+ siteStyle: (projectState?.projectConfig?.style as Record<string, unknown>) ?? null,
289
+ };
623
290
  }