@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -5,11 +5,12 @@
5
5
  * multiple canvas-related modules: element lookup, zoom, panel resolution, inline bubbling.
6
6
  */
7
7
 
8
- import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath } from "../store";
8
+ import { canvasPanels, elToPath, getNodeAtPath, parentElementPath, pathsEqual } from "../store";
9
9
  import { activeTab } from "../workspace/workspace";
10
10
  import { isInlineInContext } from "../editor/inline-edit";
11
11
  import type { JxPath } from "../state";
12
12
  import type { JxMutableNode } from "@jxsuite/schema/types";
13
+ import type { CanvasPanel } from "../panels/canvas-dnd.js";
13
14
 
14
15
  let _ctx: { getCanvasMode: () => string; getZoom: () => number } | null = null;
15
16
 
@@ -30,14 +31,33 @@ export function effectiveZoom() {
30
31
  : (ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
31
32
  }
32
33
 
34
+ /**
35
+ * Convert a canvas panel's mediaName to an activeMedia value. Panels without a breakpoint (content
36
+ * mode, single-panel docs) carry "" or "base" — both mean the base style context (null).
37
+ *
38
+ * @param {string | null | undefined} mediaName
39
+ * @returns {string | null}
40
+ */
41
+ export function panelMediaToActiveMedia(mediaName: string | null | undefined) {
42
+ return !mediaName || mediaName === "base" ? null : mediaName;
43
+ }
44
+
33
45
  /** Return the active canvas panel based on the current activeMedia setting. */
34
46
  export function getActivePanel() {
35
- if (canvasPanels.length === 0) return null;
36
- if (canvasPanels.length === 1) return canvasPanels[0];
47
+ if (canvasPanels.length === 0) {
48
+ return null;
49
+ }
50
+ if (canvasPanels.length === 1) {
51
+ return canvasPanels[0];
52
+ }
37
53
  const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
38
54
  for (const p of canvasPanels) {
39
- if (activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
40
- if (p.mediaName === activeMedia) return p;
55
+ if (activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) {
56
+ return p;
57
+ }
58
+ if (p.mediaName === activeMedia) {
59
+ return p;
60
+ }
41
61
  }
42
62
  return canvasPanels[0];
43
63
  }
@@ -50,16 +70,22 @@ export function getActivePanel() {
50
70
  * @param {JxPath} path
51
71
  */
52
72
  export function bubbleInlinePath(doc: JxMutableNode | undefined, path: JxPath) {
53
- if (!doc) return path;
73
+ if (!doc) {
74
+ return path;
75
+ }
54
76
  let currentPath = path;
55
77
  while (currentPath.length >= 2) {
56
78
  const node = getNodeAtPath(doc, currentPath);
57
79
  const pPath = parentElementPath(currentPath);
58
80
  const parentNode = pPath ? getNodeAtPath(doc, pPath) : null;
59
- if (!node || !parentNode) break;
81
+ if (!node || !parentNode) {
82
+ break;
83
+ }
60
84
  const childTag = (node.tagName ?? "div").toLowerCase();
61
85
  const parentTag = (parentNode.tagName ?? "div").toLowerCase();
62
- if (!isInlineInContext(childTag, parentTag)) break;
86
+ if (!isInlineInContext(childTag, parentTag)) {
87
+ break;
88
+ }
63
89
  currentPath = pPath as JxPath;
64
90
  }
65
91
  return currentPath;
@@ -74,11 +100,17 @@ export function bubbleInlinePath(doc: JxMutableNode | undefined, path: JxPath) {
74
100
  */
75
101
  export function findCanvasElement(path: JxPath, canvasEl: HTMLElement) {
76
102
  let el: HTMLElement | null | undefined = canvasEl.firstElementChild as HTMLElement | null;
77
- if (!el) return null;
78
- if (path.length === 0) return el;
103
+ if (!el) {
104
+ return null;
105
+ }
106
+ if (path.length === 0) {
107
+ return el;
108
+ }
79
109
 
80
110
  for (let i = 0; i < path.length; i += 2) {
81
- if (path[i] !== "children" && path[i] !== "cases") return null;
111
+ if (path[i] !== "children" && path[i] !== "cases") {
112
+ return null;
113
+ }
82
114
  const idx = path[i + 1];
83
115
  if (idx === undefined) {
84
116
  el = el.children[0] as HTMLElement | undefined;
@@ -87,17 +119,23 @@ export function findCanvasElement(path: JxPath, canvasEl: HTMLElement) {
87
119
  } else {
88
120
  el = el.children[idx as number] as HTMLElement | undefined;
89
121
  }
90
- if (!el) break;
122
+ if (!el) {
123
+ break;
124
+ }
91
125
  }
92
126
 
93
127
  if (el) {
94
128
  const elPath = elToPath.get(el);
95
- if (elPath && pathsEqual(elPath, path)) return el;
129
+ if (elPath && pathsEqual(elPath, path)) {
130
+ return el;
131
+ }
96
132
  }
97
133
 
98
134
  for (const candidate of canvasEl.querySelectorAll("*")) {
99
135
  const p = elToPath.get(candidate);
100
- if (p && pathsEqual(p, path)) return candidate as HTMLElement;
136
+ if (p && pathsEqual(p, path)) {
137
+ return candidate as HTMLElement;
138
+ }
101
139
  }
102
140
  return null;
103
141
  }
@@ -109,20 +147,16 @@ export function findCanvasElement(path: JxPath, canvasEl: HTMLElement) {
109
147
  * @param {string} type
110
148
  * @param {import("../panels/canvas-dnd.js").CanvasPanel} panel
111
149
  */
112
- export function overlayBoxDescriptor(
113
- el: Element,
114
- type: string,
115
- panel: import("../panels/canvas-dnd.js").CanvasPanel,
116
- ) {
150
+ export function overlayBoxDescriptor(el: Element, type: string, panel: CanvasPanel) {
117
151
  const viewport = panel.viewport as HTMLElement;
118
152
  const vpRect = viewport.getBoundingClientRect();
119
153
  const elRect = el.getBoundingClientRect();
120
154
  const scale = effectiveZoom();
121
155
  return {
122
156
  cls: `overlay-box overlay-${type}`,
123
- top: `${(elRect.top - vpRect.top + viewport.scrollTop) / scale}px`,
157
+ height: `${elRect.height / scale}px`,
124
158
  left: `${(elRect.left - vpRect.left + viewport.scrollLeft) / scale}px`,
159
+ top: `${(elRect.top - vpRect.top + viewport.scrollTop) / scale}px`,
125
160
  width: `${elRect.width / scale}px`,
126
- height: `${elRect.height / scale}px`,
127
161
  };
128
162
  }
@@ -6,33 +6,35 @@
6
6
  * mapping ($map remapping), site-level style injection, and $head element injection.
7
7
  */
8
8
 
9
- import { elToPath, stripEventHandlers, projectState } from "../store";
9
+ import { elToPath, projectState, stripEventHandlers } from "../store";
10
+ import { errorMessage } from "@jxsuite/schema/parse";
10
11
  import { activeTab } from "../workspace/workspace";
11
12
  import { view } from "../view";
12
13
  import { toRaw } from "../reactivity";
13
14
  import {
14
- renderNode as runtimeRenderNode,
15
15
  buildScope,
16
16
  defineElement,
17
+ renderNode as runtimeRenderNode,
17
18
  setSkipServerFunctions,
18
19
  } from "@jxsuite/runtime";
19
20
  import {
21
+ distributePageIntoLayout,
20
22
  getEffectiveElements,
21
- getEffectiveImports,
22
- getEffectiveMedia,
23
23
  getEffectiveHead,
24
+ getEffectiveImports,
24
25
  getEffectiveLayoutPath,
26
+ getEffectiveMedia,
25
27
  resolveLayoutDoc,
26
- distributePageIntoLayout,
27
28
  } from "../site-context";
28
29
  import { componentRegistry, computeRelativePath } from "../files/components";
29
30
  import { prepareForEditMode } from "../utils/edit-display";
30
31
  import { getActiveElement } from "../editor/inline-edit";
31
32
  import { buildNestedSiteCSS } from "./nested-site-style";
32
33
 
33
- export { buildNestedSiteCSS } from "./nested-site-style";
34
+ import type { JxDocument, JxElement, JxMutableNode } from "@jxsuite/schema/types";
35
+ import type { ComponentEntry } from "../files/components.js";
34
36
 
35
- import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
37
+ export { buildNestedSiteCSS } from "./nested-site-style";
36
38
 
37
39
  /** @param {Event} e */
38
40
  function _preventNav(e: Event) {
@@ -67,10 +69,11 @@ function findPageContentPrefix(
67
69
  node: JxMutableNode,
68
70
  path: (string | number)[] = [],
69
71
  ): (string | number)[] | null {
70
- if (!node || typeof node !== "object") return null;
72
+ if (!node || typeof node !== "object") {
73
+ return null;
74
+ }
71
75
  if (Array.isArray(node.children)) {
72
- for (let i = 0; i < node.children.length; i++) {
73
- const child = node.children[i];
76
+ for (const child of node.children) {
74
77
  if (child && typeof child === "object" && !child.$__layout) {
75
78
  return [...path, "children"];
76
79
  }
@@ -79,7 +82,9 @@ function findPageContentPrefix(
79
82
  const child = node.children[i];
80
83
  if (child && typeof child === "object" && child.$__layout) {
81
84
  const found = findPageContentPrefix(child, [...path, "children", i]);
82
- if (found) return found;
85
+ if (found) {
86
+ return found;
87
+ }
83
88
  }
84
89
  }
85
90
  }
@@ -94,17 +99,23 @@ export let activeLayoutPath = null as string | null;
94
99
  * rendered DOM elements came from the layout vs page content.
95
100
  */
96
101
  function markLayoutNodes(node: JxMutableNode) {
97
- if (!node || typeof node !== "object") return;
102
+ if (!node || typeof node !== "object") {
103
+ return;
104
+ }
98
105
  node.$__layout = true;
99
106
  if (Array.isArray(node.children)) {
100
107
  for (const child of node.children) {
101
- if (typeof child === "string") continue;
108
+ if (typeof child === "string") {
109
+ continue;
110
+ }
102
111
  markLayoutNodes(child);
103
112
  }
104
113
  }
105
114
  if (node.$elements) {
106
115
  for (const el of node.$elements) {
107
- if (typeof el !== "string") markLayoutNodes(el);
116
+ if (typeof el !== "string") {
117
+ markLayoutNodes(el as JxMutableNode);
118
+ }
108
119
  }
109
120
  }
110
121
  }
@@ -132,9 +143,9 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
132
143
  export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl: HTMLElement) {
133
144
  const tab = activeTab.value;
134
145
  const S = {
146
+ document: tab?.doc.document,
135
147
  documentPath: tab?.documentPath,
136
148
  mode: tab?.doc.mode,
137
- document: tab?.doc.document,
138
149
  };
139
150
  const canvasMode = _ctx!.getCanvasMode();
140
151
 
@@ -144,8 +155,8 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
144
155
  }
145
156
 
146
157
  // Suppress server function resolution in non-preview modes to avoid
147
- // failed proxy calls and infinite reactive retries (also covers
148
- // async custom element connectedCallbacks that run after this function returns)
158
+ // Failed proxy calls and infinite reactive retries (also covers
159
+ // Async custom element connectedCallbacks that run after this function returns)
149
160
  setSkipServerFunctions(canvasMode !== "preview");
150
161
 
151
162
  let renderDoc =
@@ -172,7 +183,9 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
172
183
  if (layoutPath) {
173
184
  const layoutDoc = await resolveLayoutDoc(layoutPath);
174
185
  if (layoutDoc) {
175
- if (gen !== view.renderGeneration) return null;
186
+ if (gen !== view.renderGeneration) {
187
+ return null;
188
+ }
176
189
  activeLayoutPath = layoutPath.replace(/^\.\//, "");
177
190
  markLayoutNodes(layoutDoc);
178
191
  const pageForSlots = canvasMode === "preview" ? structuredClone(toRaw(doc)) : renderDoc;
@@ -186,11 +199,13 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
186
199
  }
187
200
 
188
201
  // In edit mode, collect paths where $map templates were inlined as children[0]
189
- // so we can remap runtime paths (children,0,...) → (children,map,...)
202
+ // So we can remap runtime paths (children,0,...) → (children,map,...)
190
203
  const mapParentPaths = new Set();
191
204
  if (canvasMode === "design" || canvasMode === "edit") {
192
205
  (function findMapParents(node: JxMutableNode, path: (string | number)[]) {
193
- if (!node || typeof node !== "object") return;
206
+ if (!node || typeof node !== "object") {
207
+ return;
208
+ }
194
209
  if (
195
210
  node.children &&
196
211
  typeof node.children === "object" &&
@@ -201,7 +216,9 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
201
216
  if (Array.isArray(node.children)) {
202
217
  for (let i = 0; i < node.children.length; i++) {
203
218
  const child = node.children[i];
204
- if (typeof child === "string") continue;
219
+ if (typeof child === "string") {
220
+ continue;
221
+ }
205
222
  findMapParents(child, [...path, "children", i]);
206
223
  }
207
224
  }
@@ -219,32 +236,38 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
219
236
  const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
220
237
 
221
238
  // Register custom elements so the runtime can render them
222
- let effectiveElements = getEffectiveElements(renderDoc.$elements as (JxElement | string)[]);
239
+ const effectiveElements = getEffectiveElements(renderDoc.$elements as (JxElement | string)[]);
223
240
 
224
241
  // In content mode (markdown) or when a layout is applied, auto-discover components
225
- // for custom elements that have no explicit $elements registration.
242
+ // For custom elements that have no explicit $elements registration.
226
243
  if ((S.mode === "content" || layoutWrapped) && componentRegistry.length > 0) {
227
244
  const existingRefs = new Set(
228
245
  effectiveElements.map((e: JxElement | string) => (typeof e === "string" ? e : e?.$ref)),
229
246
  );
230
247
  /** @param {JxMutableNode} node */
231
248
  const collectTags = (node: JxMutableNode) => {
232
- const tags: Set<string> = new Set();
233
- if (!node || typeof node !== "object") return tags;
234
- if (node.tagName) tags.add(node.tagName);
249
+ const tags = new Set<string>();
250
+ if (!node || typeof node !== "object") {
251
+ return tags;
252
+ }
253
+ if (node.tagName) {
254
+ tags.add(node.tagName);
255
+ }
235
256
  if (Array.isArray(node.children)) {
236
257
  for (const child of node.children) {
237
- if (typeof child === "string") continue;
238
- for (const t of collectTags(child)) tags.add(t);
258
+ if (typeof child === "string") {
259
+ continue;
260
+ }
261
+ for (const t of collectTags(child)) {
262
+ tags.add(t);
263
+ }
239
264
  }
240
265
  }
241
266
  return tags;
242
267
  };
243
268
  for (const tag of collectTags(renderDoc)) {
244
- const comp = componentRegistry.find(
245
- (c: import("../files/components.js").ComponentEntry) => c.tagName === tag,
246
- );
247
- if (comp && comp.source !== "npm") {
269
+ const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
270
+ if (comp && comp.source !== "npm" && comp.path) {
248
271
  const relPath = computeRelativePath(S.documentPath ?? null, comp.path);
249
272
  if (!existingRefs.has(relPath)) {
250
273
  effectiveElements.push({ $ref: relPath });
@@ -254,7 +277,7 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
254
277
  }
255
278
  }
256
279
 
257
- if (effectiveElements.length) {
280
+ if (effectiveElements.length > 0) {
258
281
  renderDoc.$elements = effectiveElements as (string | JxMutableNode | { $ref: string })[];
259
282
  for (const entry of effectiveElements) {
260
283
  if (typeof entry === "string") {
@@ -262,39 +285,43 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
262
285
  const specifier =
263
286
  entry.startsWith("/") || entry.startsWith(".") ? entry : `/node_modules/${entry}`;
264
287
  await import(specifier);
265
- } catch (e) {
266
- console.warn("Studio: failed to import package", entry, e);
288
+ } catch (error) {
289
+ console.warn("Studio: failed to import package", entry, error);
267
290
  }
268
291
  } else if (entry?.$ref) {
269
292
  let href;
270
293
  try {
271
- href = new URL(entry.$ref, docBase).href;
272
- } catch (urlErr) {
273
- console.warn("Studio: invalid element URL", { ref: entry.$ref, docBase }, urlErr);
294
+ ({ href } = new URL(entry.$ref, docBase));
295
+ } catch (error) {
296
+ console.warn("Studio: invalid element URL", { docBase, ref: entry.$ref }, error);
297
+ continue;
298
+ }
299
+ if (_failedElements.has(href)) {
274
300
  continue;
275
301
  }
276
- if (_failedElements.has(href)) continue;
277
302
  try {
278
303
  await defineElement(href);
279
- } catch (e) {
304
+ } catch (error) {
280
305
  _failedElements.add(href);
281
- console.warn("Studio: failed to register element", entry.$ref, e);
306
+ console.warn("Studio: failed to register element", entry.$ref, error);
282
307
  }
283
308
  }
284
309
  }
285
310
  }
286
311
 
287
312
  // Bail out if a newer render started while we were importing elements
288
- if (gen !== view.renderGeneration) return null;
313
+ if (gen !== view.renderGeneration) {
314
+ return null;
315
+ }
289
316
 
290
317
  // Inject site-level imports so buildScope can resolve $prototype names
291
318
  renderDoc.imports = getEffectiveImports(renderDoc.imports);
292
319
 
293
320
  // Apply project-level styles mirroring the compiler convention:
294
- // viewport ≈ :root → CSS custom properties (they inherit down)
295
- // canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
321
+ // Viewport ≈ :root → CSS custom properties (they inherit down)
322
+ // CanvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
296
323
  // This ensures project font-family, color, etc. override the
297
- // content-mode fallback typography rules in the stylesheet.
324
+ // Content-mode fallback typography rules in the stylesheet.
298
325
  // In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
299
326
  const viewport = canvasEl.closest(".canvas-panel-viewport") as HTMLElement | null;
300
327
  const editSurface =
@@ -306,7 +333,9 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
306
333
  viewport.style.cssText = "";
307
334
  if (siteStyle && typeof siteStyle === "object") {
308
335
  for (const [k, v] of Object.entries(siteStyle)) {
309
- if (v !== null && typeof v === "object" && !Array.isArray(v)) continue;
336
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
337
+ continue;
338
+ }
310
339
  if (k.startsWith("--")) {
311
340
  viewport.style.setProperty(k, String(v));
312
341
  } else {
@@ -315,21 +344,23 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
315
344
  }
316
345
  }
317
346
  }
318
- if (editSurface) {
319
- if (siteStyle && typeof siteStyle === "object") {
320
- for (const [k, v] of Object.entries(siteStyle)) {
321
- if (v !== null && typeof v === "object" && !Array.isArray(v)) continue;
322
- if (k.startsWith("--")) {
323
- editSurface.style.setProperty(k, String(v));
324
- } else {
325
- (editSurface.style as unknown as Record<string, string>)[k] = String(v);
326
- }
347
+ if (editSurface && siteStyle && typeof siteStyle === "object") {
348
+ for (const [k, v] of Object.entries(siteStyle)) {
349
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
350
+ continue;
351
+ }
352
+ if (k.startsWith("--")) {
353
+ editSurface.style.setProperty(k, String(v));
354
+ } else {
355
+ (editSurface.style as unknown as Record<string, string>)[k] = String(v);
327
356
  }
328
357
  }
329
358
  }
330
359
  if (siteStyle && typeof siteStyle === "object") {
331
360
  for (const [k, v] of Object.entries(siteStyle)) {
332
- if (v !== null && typeof v === "object" && !Array.isArray(v)) continue;
361
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
362
+ continue;
363
+ }
333
364
  if (!k.startsWith("--")) {
334
365
  (canvasEl.style as unknown as Record<string, string>)[k] = String(v);
335
366
  }
@@ -343,12 +374,14 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
343
374
  const css = buildNestedSiteCSS(siteStyle, `[${scopeAttr}]`);
344
375
 
345
376
  if (css) {
346
- const existingStyleEl = document.getElementById("jx-site-style");
347
- if (existingStyleEl) existingStyleEl.remove();
377
+ const existingStyleEl = document.querySelector("#jx-site-style");
378
+ if (existingStyleEl) {
379
+ existingStyleEl.remove();
380
+ }
348
381
  const styleEl = document.createElement("style");
349
382
  styleEl.id = "jx-site-style";
350
383
  styleEl.textContent = css;
351
- document.head.appendChild(styleEl);
384
+ document.head.append(styleEl);
352
385
  }
353
386
  }
354
387
 
@@ -357,13 +390,17 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
357
390
 
358
391
  // Inject $head elements (link/meta/script) into document.head
359
392
  const effectiveHead = getEffectiveHead(renderDoc.$head);
360
- if (effectiveHead.length) {
393
+ if (effectiveHead.length > 0) {
361
394
  for (const entry of effectiveHead) {
362
- if (!entry?.tagName) continue;
395
+ if (!entry?.tagName) {
396
+ continue;
397
+ }
363
398
  const tag = entry.tagName.toLowerCase();
364
399
  // Skip inline scripts — they can contain arbitrary JS/HTML that throws
365
- // when the browser tries to execute it in the studio context
366
- if (tag === "script" && !entry.attributes?.src) continue;
400
+ // When the browser tries to execute it in the studio context
401
+ if (tag === "script" && !entry.attributes?.src) {
402
+ continue;
403
+ }
367
404
  const attrs = { ...entry.attributes };
368
405
  for (const key of ["href", "src"]) {
369
406
  const val = attrs[key];
@@ -377,34 +414,45 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
377
414
  }
378
415
  }
379
416
  const selector = `${tag}${attrs.href ? `[href="${attrs.href}"]` : ""}${attrs.src ? `[src="${attrs.src}"]` : ""}`;
380
- if (selector !== tag && document.head.querySelector(selector)) continue;
417
+ if (selector !== tag && document.head.querySelector(selector)) {
418
+ continue;
419
+ }
381
420
  const el = document.createElement(tag);
382
- for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
383
- if (entry.textContent) el.textContent = entry.textContent;
384
- document.head.appendChild(el);
421
+ for (const [k, v] of Object.entries(attrs)) {
422
+ el.setAttribute(k, String(v));
423
+ }
424
+ if (entry.textContent) {
425
+ el.textContent = entry.textContent;
426
+ }
427
+ document.head.append(el);
385
428
  }
386
429
  }
387
430
 
388
- const $defs = await buildScope(renderDoc, {}, docBase);
431
+ const $defs = await buildScope(renderDoc as JxDocument, {}, docBase);
389
432
  // Bail out if a newer render started while buildScope was running
390
- if (gen !== view.renderGeneration) return null;
433
+ if (gen !== view.renderGeneration) {
434
+ return null;
435
+ }
391
436
  const el = /** @type {HTMLElement} */ runtimeRenderNode(renderDoc, $defs, {
437
+ _path: [],
392
438
  onNodeCreated(
393
- el: HTMLElement | Text,
439
+ created: HTMLElement | Text,
394
440
  path: (string | number)[],
395
- def: Record<string, unknown>,
441
+ def: JxElement | string,
396
442
  ) {
397
- if (!(el instanceof HTMLElement)) return;
443
+ if (!(created instanceof HTMLElement)) {
444
+ return;
445
+ }
398
446
  // Track layout-originated elements — don't store in elToPath to avoid
399
- // path collisions with remapped page content paths
400
- if (layoutWrapped && def?.$__layout) {
401
- layoutElements.add(el);
402
- if (el.setAttribute) el.setAttribute("data-jx-layout", "");
447
+ // Path collisions with remapped page content paths
448
+ if (layoutWrapped && typeof def === "object" && def?.$__layout) {
449
+ layoutElements.add(created);
450
+ created.dataset.jxLayout = "";
403
451
  return;
404
452
  }
405
453
 
406
454
  // Remap layout-wrapped paths: strip the layout prefix so paths are
407
- // relative to the original page document (which is what S.document holds)
455
+ // Relative to the original page document (which is what S.document holds)
408
456
  let mappedPath = path;
409
457
  if (layoutWrapped && pageContentPrefix) {
410
458
  const pfx = pageContentPrefix;
@@ -417,7 +465,7 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
417
465
  }
418
466
 
419
467
  // Remap $map paths: wrapper and template children → real document paths
420
- // prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
468
+ // PrepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
421
469
  // Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
422
470
  if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
423
471
  for (let i = 0; i < mappedPath.length - 1; i++) {
@@ -443,11 +491,10 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
443
491
  }
444
492
  }
445
493
  }
446
- elToPath.set(el, mappedPath);
494
+ elToPath.set(created, mappedPath);
447
495
  },
448
- _path: [],
449
496
  });
450
- if (canvasMode === "design" || canvasMode === "edit") {
497
+ if ((canvasMode === "design" || canvasMode === "edit") && el instanceof HTMLElement) {
451
498
  // Disable pointer events on all rendered elements for edit mode
452
499
  el.style.pointerEvents = "none";
453
500
  for (const child of el.querySelectorAll("*")) {
@@ -455,15 +502,15 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
455
502
  }
456
503
  }
457
504
  // Clear and append atomically — ensures the canvas is never left empty if a
458
- // newer render starts and this one would have bailed after clearing.
505
+ // Newer render starts and this one would have bailed after clearing.
459
506
  canvasEl.innerHTML = "";
460
507
  if (S.mode === "content") {
461
- canvasEl.setAttribute("data-content-mode", "");
508
+ canvasEl.dataset.contentMode = "";
462
509
  } else {
463
- canvasEl.removeAttribute("data-content-mode");
510
+ delete canvasEl.dataset.contentMode;
464
511
  }
465
512
 
466
- canvasEl.appendChild(el);
513
+ canvasEl.append(el);
467
514
 
468
515
  // Delegated click handler prevents link navigation in all canvas modes.
469
516
  // Attached once per canvasEl (survives reactive re-renders that replace children).
@@ -476,15 +523,19 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
476
523
  requestAnimationFrame(() => {
477
524
  const editingEl = getActiveElement();
478
525
  for (const child of canvasEl.querySelectorAll("*")) {
479
- if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
480
- if (editingEl && child === editingEl) continue;
526
+ if (view.componentInlineEdit && child === view.componentInlineEdit.el) {
527
+ continue;
528
+ }
529
+ if (editingEl && child === editingEl) {
530
+ continue;
531
+ }
481
532
  (child as HTMLElement).style.pointerEvents = "none";
482
533
  }
483
534
  });
484
535
  }
485
536
  return $defs;
486
- } catch (err) {
487
- console.warn("renderCanvasLive failed:", (err as Error).message, err);
537
+ } catch (error) {
538
+ console.warn("renderCanvasLive failed:", errorMessage(error), error);
488
539
  return null;
489
540
  }
490
541
  }