@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.
- package/dist/iframe-entry.js +6238 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +30972 -17222
- package/dist/studio.js.map +328 -83
- package/package.json +9 -9
- 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 +5 -1
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/page-params.ts +383 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +189 -457
- 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/git-panel.ts +16 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +21 -7
- 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 +45 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/services/settings-store.ts +93 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +115 -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 +2 -74
- package/src/types.ts +98 -18
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +13 -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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client for the /__studio/import-site NDJSON stream. Every platform's importSite is "a
|
|
3
|
+
* streaming fetch to some URL" — the dev server posts to its own origin, the desktop platforms post
|
|
4
|
+
* to their token-gated loopback servers — so the request/parse/settle logic lives here once.
|
|
5
|
+
*
|
|
6
|
+
* The endpoint emits one JSON object per line: `progress` lines (forwarded to onProgress),
|
|
7
|
+
* `heartbeat` keep-alives (ignored), and a terminal `done` ({root, config}) or `error` line.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ImportProgressEvent, ImportSiteOptions } from "../types";
|
|
11
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
12
|
+
|
|
13
|
+
interface StreamLine {
|
|
14
|
+
type: string;
|
|
15
|
+
phase?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
current?: number;
|
|
18
|
+
total?: number;
|
|
19
|
+
root?: string;
|
|
20
|
+
config?: ProjectConfig;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* POST an import request and consume its NDJSON progress stream.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} endpoint — the platform's import-site URL (may carry an auth token)
|
|
28
|
+
* @param {ImportSiteOptions} opts — the import request; `apiKey`/`baseUrl` travel as headers
|
|
29
|
+
* @param {(evt: ImportProgressEvent) => void} onProgress
|
|
30
|
+
* @param {AbortSignal} [signal]
|
|
31
|
+
*/
|
|
32
|
+
export async function streamImport(
|
|
33
|
+
endpoint: string,
|
|
34
|
+
opts: ImportSiteOptions,
|
|
35
|
+
onProgress: (evt: ImportProgressEvent) => void,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<{ root: string; config: ProjectConfig }> {
|
|
38
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
39
|
+
if (opts.apiKey) {
|
|
40
|
+
headers["X-Api-Key"] = opts.apiKey;
|
|
41
|
+
}
|
|
42
|
+
if (opts.baseUrl) {
|
|
43
|
+
headers["X-Api-Base-URL"] = opts.baseUrl;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const res = await fetch(endpoint, {
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
url: opts.url,
|
|
49
|
+
directory: opts.directory,
|
|
50
|
+
depth: opts.depth,
|
|
51
|
+
maxPages: opts.maxPages,
|
|
52
|
+
aiComponents: opts.aiComponents,
|
|
53
|
+
...(opts.model === undefined ? {} : { aiModel: opts.model }),
|
|
54
|
+
}),
|
|
55
|
+
headers,
|
|
56
|
+
method: "POST",
|
|
57
|
+
...(signal === undefined ? {} : { signal }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
let message = `Import failed (${res.status})`;
|
|
62
|
+
try {
|
|
63
|
+
const body = (await res.json()) as { error?: string };
|
|
64
|
+
if (body.error) {
|
|
65
|
+
message = body.error;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* Non-JSON error body — keep the status message. */
|
|
69
|
+
}
|
|
70
|
+
throw new Error(message);
|
|
71
|
+
}
|
|
72
|
+
if (!res.body) {
|
|
73
|
+
throw new Error("Import stream had no response body");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reader = res.body.getReader();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
let buffer = "";
|
|
79
|
+
let result: { root: string; config: ProjectConfig } | null = null;
|
|
80
|
+
|
|
81
|
+
const handleLine = (line: string) => {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let parsed: StreamLine;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(trimmed) as StreamLine;
|
|
89
|
+
} catch {
|
|
90
|
+
return; // Tolerate a mangled line rather than killing the whole import.
|
|
91
|
+
}
|
|
92
|
+
if (parsed.type === "progress") {
|
|
93
|
+
onProgress({
|
|
94
|
+
phase: parsed.phase ?? "",
|
|
95
|
+
message: parsed.message ?? "",
|
|
96
|
+
...(parsed.current === undefined ? {} : { current: parsed.current }),
|
|
97
|
+
...(parsed.total === undefined ? {} : { total: parsed.total }),
|
|
98
|
+
});
|
|
99
|
+
} else if (parsed.type === "done" && parsed.root !== undefined && parsed.config) {
|
|
100
|
+
result = { root: parsed.root, config: parsed.config };
|
|
101
|
+
} else if (parsed.type === "error") {
|
|
102
|
+
throw new Error(parsed.error || "Import failed");
|
|
103
|
+
}
|
|
104
|
+
// Heartbeats and unknown line types are ignored.
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
// Lines can split across chunks: keep the trailing partial in the buffer.
|
|
115
|
+
const lines = buffer.split("\n");
|
|
116
|
+
buffer = lines.pop() ?? "";
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
handleLine(line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
handleLine(buffer);
|
|
122
|
+
} finally {
|
|
123
|
+
try {
|
|
124
|
+
await reader.cancel();
|
|
125
|
+
} catch {
|
|
126
|
+
/* The stream is already closed (e.g. after an abort). */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!result) {
|
|
131
|
+
throw new Error("Import stream ended without a result");
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -23,6 +23,18 @@ self.MonacoEnvironment = {
|
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/* Monaco caches glyph widths when an editor is created. JetBrains Mono is a
|
|
27
|
+
vendored webfont (index.html @font-face) and may finish loading after the
|
|
28
|
+
first editor mounts, so remeasure once all document fonts are ready to keep
|
|
29
|
+
cursor and selection alignment correct. */
|
|
30
|
+
if (typeof document !== "undefined" && document.fonts?.ready) {
|
|
31
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- fire-and-forget: awaiting fonts.ready at top level would block module evaluation (and studio startup) until webfonts load
|
|
32
|
+
void document.fonts.ready.then(async () => {
|
|
33
|
+
const monaco = await import("monaco-editor/esm/vs/editor/editor.api.js");
|
|
34
|
+
monaco.editor.remeasureFonts();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
// oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
|
|
27
39
|
jsonDefaults.setDiagnosticsOptions({
|
|
28
40
|
allowComments: false,
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
* @license MIT
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { buildScope, renderNode, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
16
|
+
import { buildScope, renderNode, runScoped, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
17
17
|
import type { JxDocument } from "@jxsuite/schema/types";
|
|
18
|
-
import { effectScope } from "../reactivity";
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Translate a render-time error into an actionable message the model can fix.
|
|
@@ -51,25 +50,26 @@ function translateRenderError(err: Error): string {
|
|
|
51
50
|
export async function renderCheck(
|
|
52
51
|
doc: JxDocument,
|
|
53
52
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
54
|
-
|
|
53
|
+
let stopRender: (() => void) | null = null;
|
|
55
54
|
try {
|
|
56
55
|
setSkipServerFunctions(true);
|
|
57
56
|
|
|
58
57
|
const state = await buildScope(doc, {});
|
|
59
58
|
|
|
60
59
|
const container = document.createElement("div");
|
|
61
|
-
|
|
60
|
+
const { stop } = runScoped(() => {
|
|
62
61
|
container.append(renderNode(doc, state));
|
|
63
62
|
});
|
|
63
|
+
stopRender = stop;
|
|
64
64
|
|
|
65
65
|
return { ok: true };
|
|
66
66
|
} catch (error) {
|
|
67
67
|
return { ok: false, error: translateRenderError(error as Error) };
|
|
68
68
|
} finally {
|
|
69
|
-
// Dispose all effects created synchronously by renderNode
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
69
|
+
// Dispose all effects created synchronously by renderNode via the RUNTIME's runScoped — scope
|
|
70
|
+
// Collection is per @vue/reactivity module instance, so a studio effectScope here would collect
|
|
71
|
+
// Nothing. Effects from the async buildScope (prototype/computed setup) still escape the scope;
|
|
72
|
+
// They're GC-eligible once the throwaway state/container are unreferenced. Bounded, acceptable.
|
|
73
|
+
stopRender?.();
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Settings-store.js — sync of user settings between localStorage and the platform backend.
|
|
4
|
+
*
|
|
5
|
+
* LocalStorage stays the synchronous source of truth for every consumer (ai-settings getters read
|
|
6
|
+
* it directly). On platforms with a backend user-settings store (desktop/chromium — where
|
|
7
|
+
* localStorage does not roam across browser profiles), the persisted keys are hydrated from the
|
|
8
|
+
* backend at boot and written through on every change. The dev server omits the platform methods
|
|
9
|
+
* and settings remain localStorage-only.
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
15
|
+
|
|
16
|
+
/** The localStorage keys mirrored into the platform's user-settings store. */
|
|
17
|
+
export const PERSISTED_SETTINGS_KEYS = ["jx.ai.openaiKey", "jx.ai.baseUrl", "jx.ai.model"] as const;
|
|
18
|
+
|
|
19
|
+
/** Read a localStorage value defensively, treating unavailable/throwing storage as empty. */
|
|
20
|
+
function readLocal(key: string): string {
|
|
21
|
+
try {
|
|
22
|
+
return globalThis.localStorage?.getItem(key) ?? "";
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pull the persisted settings from the backend store into localStorage. Call once after the
|
|
30
|
+
* platform is registered; a no-op on platforms without a settings store (dev server).
|
|
31
|
+
*
|
|
32
|
+
* Migration: keys the backend does not know yet but localStorage does (an existing user upgrading
|
|
33
|
+
* from localStorage-only builds) are pushed back to the backend once, fire-and-forget.
|
|
34
|
+
*/
|
|
35
|
+
export async function hydrateSettings(): Promise<void> {
|
|
36
|
+
if (!hasPlatform()) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const platform = getPlatform();
|
|
40
|
+
if (!platform.getSettings) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let stored: Record<string, string>;
|
|
44
|
+
try {
|
|
45
|
+
stored = await platform.getSettings();
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const merged: Record<string, string> = { ...stored };
|
|
50
|
+
let needsMigration = false;
|
|
51
|
+
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
52
|
+
const backendValue = stored[key];
|
|
53
|
+
if (backendValue) {
|
|
54
|
+
try {
|
|
55
|
+
globalThis.localStorage?.setItem(key, backendValue);
|
|
56
|
+
} catch {
|
|
57
|
+
/* Storage unavailable — consumers fall back to defaults. */
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const localValue = readLocal(key);
|
|
61
|
+
if (localValue) {
|
|
62
|
+
merged[key] = localValue;
|
|
63
|
+
needsMigration = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (needsMigration && platform.saveSettings) {
|
|
68
|
+
void platform.saveSettings(merged).catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Write the current values of the persisted keys through to the backend store, fire-and-forget.
|
|
74
|
+
* Call after mutating any of {@link PERSISTED_SETTINGS_KEYS} in localStorage; a no-op on platforms
|
|
75
|
+
* without a settings store (dev server).
|
|
76
|
+
*/
|
|
77
|
+
export function persistSettings(): void {
|
|
78
|
+
if (!hasPlatform()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const platform = getPlatform();
|
|
82
|
+
if (!platform.saveSettings) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const settings: Record<string, string> = {};
|
|
86
|
+
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
87
|
+
const value = readLocal(key);
|
|
88
|
+
if (value) {
|
|
89
|
+
settings[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
void platform.saveSettings(settings).catch(() => {});
|
|
93
|
+
}
|
|
@@ -114,7 +114,7 @@ function renderColorSection(
|
|
|
114
114
|
<div class="css-var-swatch" style="background:${val}">
|
|
115
115
|
<input
|
|
116
116
|
type="color"
|
|
117
|
-
.value=${val && String(val).startsWith("#") ? val : "#
|
|
117
|
+
.value=${val && String(val).startsWith("#") ? val : "#3b82f6"}
|
|
118
118
|
@input=${(e: Event) => updateVar(name, (e.target as HTMLInputElement).value)}
|
|
119
119
|
/>
|
|
120
120
|
</div>
|
|
@@ -131,7 +131,7 @@ function renderColorSection(
|
|
|
131
131
|
</div>
|
|
132
132
|
`,
|
|
133
133
|
)}
|
|
134
|
-
${renderAddRow("--color-", "Primary Blue", "#
|
|
134
|
+
${renderAddRow("--color-", "Primary Blue", "#3b82f6", addVar)}
|
|
135
135
|
</div>
|
|
136
136
|
`;
|
|
137
137
|
}
|
package/src/store.ts
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
// ─── Re-exports from state.js ────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
import { activeTab } from "./workspace/workspace";
|
|
13
|
-
import { isEventBinding } from "@jxsuite/schema/guards";
|
|
14
13
|
import type { JxPath } from "./state";
|
|
15
14
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
16
|
-
import type { CanvasPanel } from "./
|
|
15
|
+
import type { CanvasPanel } from "./types";
|
|
17
16
|
|
|
18
17
|
export {
|
|
19
18
|
createState,
|
|
@@ -52,20 +51,6 @@ export function initShellRefs() {
|
|
|
52
51
|
|
|
53
52
|
// ─── Shared containers (mutated in place by owner modules) ───────────────────
|
|
54
53
|
|
|
55
|
-
export const elToPath = new WeakMap<Element, JxPath>();
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Canvas element → the runtime scope its children render with. Captured during live renders so the
|
|
59
|
-
* canvas patcher can re-render a subtree in isolation with the same prototype-chained scope.
|
|
60
|
-
*/
|
|
61
|
-
export const elToScope = new WeakMap<Element, Record<string, unknown>>();
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Subtree root element → the effect scope created for its surgical render. Stopped when the subtree
|
|
65
|
-
* is replaced or removed so render-time reactive effects don't accumulate.
|
|
66
|
-
*/
|
|
67
|
-
export const elToRenderScope = new WeakMap<Element, { stop: () => void }>();
|
|
68
|
-
|
|
69
54
|
export const canvasPanels: CanvasPanel[] = [];
|
|
70
55
|
|
|
71
56
|
// ─── Shared constants ────────────────────────────────────────────────────────
|
|
@@ -139,52 +124,9 @@ export function cancelStyleDebounce(prop: string) {
|
|
|
139
124
|
_styleDebounceTimers.delete(prop);
|
|
140
125
|
}
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* @param {JxMutableNode} node
|
|
146
|
-
* @returns {JxMutableNode}
|
|
147
|
-
*/
|
|
148
|
-
export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
|
|
149
|
-
if (!node || typeof node !== "object") {
|
|
150
|
-
return node;
|
|
151
|
-
}
|
|
152
|
-
if (Array.isArray(node)) {
|
|
153
|
-
// Arrays of nodes round-trip element-wise; the array itself is not a node.
|
|
154
|
-
return node.map((n: JxMutableNode) => stripEventHandlers(n)) as unknown as JxMutableNode;
|
|
155
|
-
}
|
|
156
|
-
const out: Record<string, unknown> = {};
|
|
157
|
-
for (const [k, v] of Object.entries(node)) {
|
|
158
|
-
if (k.startsWith("on") && isEventBinding(v)) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (k === "children") {
|
|
162
|
-
out.children = Array.isArray(v)
|
|
163
|
-
? v.map((c: JxMutableNode) => stripEventHandlers(c))
|
|
164
|
-
: stripEventHandlers(v as JxMutableNode);
|
|
165
|
-
} else if (k === "cases" && typeof v === "object") {
|
|
166
|
-
const cases: Record<string, unknown> = {};
|
|
167
|
-
for (const [ck, cv] of Object.entries(v as Record<string, unknown>)) {
|
|
168
|
-
cases[ck] = stripEventHandlers(cv as JxMutableNode);
|
|
169
|
-
}
|
|
170
|
-
out.cases = cases;
|
|
171
|
-
} else if (k === "state" && typeof v === "object" && v !== null) {
|
|
172
|
-
const state: Record<string, unknown> = {};
|
|
173
|
-
for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
|
|
174
|
-
if (sv && typeof sv === "object" && (sv as Record<string, unknown>).timing === "server") {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
state[sk] = sv;
|
|
178
|
-
}
|
|
179
|
-
out.state = state;
|
|
180
|
-
} else if (k === "style" || k === "attributes" || k === "$media") {
|
|
181
|
-
out[k] = v;
|
|
182
|
-
} else {
|
|
183
|
-
out[k] = v;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
127
|
+
// `stripEventHandlers` moved to ./utils/strip-events (dependency-light, shared with the iframe
|
|
128
|
+
// Subtree renderer); re-exported here so existing `from "../store"` imports keep working.
|
|
129
|
+
export { stripEventHandlers } from "./utils/strip-events";
|
|
188
130
|
|
|
189
131
|
// ─── Render orchestration ────────────────────────────────────────────────────
|
|
190
132
|
|