@jxsuite/studio 0.32.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.
- package/dist/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +22791 -13709
- package/dist/studio.js.map +181 -74
- package/package.json +12 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +386 -328
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +18 -47
- package/src/services/ai-settings.ts +107 -0
- package/src/services/ai-system-prompt.ts +617 -0
- package/src/services/ai-tools.ts +854 -0
- package/src/services/context-manager.ts +200 -0
- package/src/services/document-assistant.ts +183 -0
- package/src/services/jx-validate.ts +65 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +75 -0
- package/src/services/token-lint.ts +140 -0
- package/src/services/tool-executor.ts +156 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/state.ts +29 -0
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +33 -69
- package/src/types.ts +16 -24
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- 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 {
|
|
10
|
-
import { errorMessage } from "@jxsuite/schema/parse";
|
|
9
|
+
import { projectState, stripEventHandlers } from "../store";
|
|
11
10
|
import { activeTab } from "../workspace/workspace";
|
|
12
|
-
import {
|
|
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 {
|
|
32
|
-
|
|
24
|
+
import {
|
|
25
|
+
paramBoundStateKeys,
|
|
26
|
+
resolveParamBoundState,
|
|
27
|
+
substitutePreviewParams,
|
|
28
|
+
} from "../page-params";
|
|
33
29
|
|
|
34
|
-
import type {
|
|
30
|
+
import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
|
|
35
31
|
import type { ComponentEntry } from "../files/components.js";
|
|
36
|
-
import type {
|
|
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
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
}
|