@jxsuite/studio 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +10754 -11060
- package/dist/studio.js.map +83 -72
- package/package.json +7 -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 +5 -7
- 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 +15 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/settings/css-vars-editor.ts +2 -2
- 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 +2 -74
- package/src/types.ts +14 -18
- 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
package/src/panels/tab-bar.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Tab bar — a per-tab
|
|
4
|
-
* Standardizes Back/breadcrumb navigation,
|
|
5
|
-
*
|
|
3
|
+
* Tab bar — a persistent per-tab settings bar rendered between the tab strip and the edit content.
|
|
4
|
+
* Standardizes Back/breadcrumb navigation, the view settings cluster (preview toggle, layout
|
|
5
|
+
* visibility, dynamic route-param pickers — shown in the edit/design base modes), media feature
|
|
6
|
+
* toggles, and mode actions (Code-mode Export) into a single bar shared by every edit mode.
|
|
6
7
|
*
|
|
7
8
|
* Follows the same module shape as tab-strip.ts: mount(host, ctx) → effectScope/effect → render().
|
|
8
|
-
* The bar
|
|
9
|
-
* contextual to show.
|
|
9
|
+
* The bar renders `nothing` (so `#tab-bar:empty` hides the row) only when no tab is active.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { html, render as litRender, nothing } from "lit-html";
|
|
13
|
-
import { updateUi } from "../store";
|
|
13
|
+
import { projectState, updateUi } from "../store";
|
|
14
14
|
import { effect, effectScope } from "../reactivity";
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
|
-
import { getEffectiveMedia } from "../site-context";
|
|
16
|
+
import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
|
|
17
|
+
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
17
18
|
import { mediaDisplayName } from "./shared";
|
|
19
|
+
import type { ParamValues } from "../page-params";
|
|
20
|
+
import type { Tab } from "../tabs/tab";
|
|
18
21
|
import type { DocumentStackEntry, FunctionEditDef } from "../types";
|
|
19
22
|
import type { EffectScope } from "@vue/reactivity";
|
|
20
23
|
import type { TemplateResult } from "lit-html";
|
|
@@ -59,12 +62,16 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
|
|
|
59
62
|
if (tab) {
|
|
60
63
|
// Read reactive properties to establish tracking — mirrors the toolbar's subset
|
|
61
64
|
void tab.doc.document;
|
|
65
|
+
void tab.doc.document?.$layout;
|
|
62
66
|
void tab.doc.mode;
|
|
63
67
|
void tab.documentPath;
|
|
64
68
|
void tab.session.documentStack.length;
|
|
65
69
|
void tab.session.ui.canvasMode;
|
|
66
70
|
void tab.session.ui.editingFunction;
|
|
67
71
|
void tab.session.ui.featureToggles;
|
|
72
|
+
void tab.session.ui.preview;
|
|
73
|
+
void tab.session.ui.previewParams;
|
|
74
|
+
void tab.session.ui.showLayout;
|
|
68
75
|
}
|
|
69
76
|
render();
|
|
70
77
|
});
|
|
@@ -143,6 +150,55 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
143
150
|
`;
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
// ── Right region: view settings cluster (edit/design base modes only) ──
|
|
154
|
+
// Gates on the BASE mode (not the effective mode): the cluster stays visible while the preview
|
|
155
|
+
// Toggle is on so it can be toggled back off.
|
|
156
|
+
const baseMode = S.ui.canvasMode;
|
|
157
|
+
const isPage = Boolean(
|
|
158
|
+
S.documentPath &&
|
|
159
|
+
projectState?.isSiteProject &&
|
|
160
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/")),
|
|
161
|
+
);
|
|
162
|
+
let settingsTpl: TemplateResult | typeof nothing = nothing;
|
|
163
|
+
if (!editing && (baseMode === "edit" || baseMode === "design")) {
|
|
164
|
+
const canPreview = tab.capabilities.modes.includes("preview");
|
|
165
|
+
const hasLayout = isPage && Boolean(getEffectiveLayoutPath(S.document?.$layout));
|
|
166
|
+
const pickersTpl = isPage ? paramPickersTpl(tab) : nothing;
|
|
167
|
+
const previewTpl = canPreview
|
|
168
|
+
? html`
|
|
169
|
+
<sp-action-button
|
|
170
|
+
toggles
|
|
171
|
+
size="s"
|
|
172
|
+
title="Preview resolved values"
|
|
173
|
+
?selected=${Boolean(S.ui.preview)}
|
|
174
|
+
@click=${() => updateUi("preview", !S.ui.preview)}
|
|
175
|
+
>
|
|
176
|
+
<sp-icon-preview slot="icon"></sp-icon-preview>
|
|
177
|
+
Preview
|
|
178
|
+
</sp-action-button>
|
|
179
|
+
`
|
|
180
|
+
: nothing;
|
|
181
|
+
const layoutTpl = hasLayout
|
|
182
|
+
? html`
|
|
183
|
+
<sp-action-button
|
|
184
|
+
toggles
|
|
185
|
+
size="s"
|
|
186
|
+
title="Show layout elements"
|
|
187
|
+
?selected=${S.ui.showLayout !== false}
|
|
188
|
+
@click=${() => updateUi("showLayout", S.ui.showLayout === false)}
|
|
189
|
+
>
|
|
190
|
+
Layout
|
|
191
|
+
</sp-action-button>
|
|
192
|
+
`
|
|
193
|
+
: nothing;
|
|
194
|
+
if (pickersTpl !== nothing || previewTpl !== nothing || layoutTpl !== nothing) {
|
|
195
|
+
settingsTpl = html`
|
|
196
|
+
${pickersTpl}
|
|
197
|
+
<sp-action-group compact size="s">${layoutTpl} ${previewTpl}</sp-action-group>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
// ── Right region: media feature toggles ──
|
|
147
203
|
const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
|
|
148
204
|
const togglesTpl =
|
|
@@ -183,16 +239,106 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
183
239
|
`
|
|
184
240
|
: nothing;
|
|
185
241
|
|
|
186
|
-
// Collapse the bar entirely when there is nothing contextual to show.
|
|
187
|
-
if (navTpl === nothing && togglesTpl === nothing && exportTpl === nothing) {
|
|
188
|
-
return nothing;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
242
|
return html`
|
|
192
243
|
<div class="tab-bar">
|
|
193
244
|
${navTpl}
|
|
194
245
|
<div class="tb-spacer"></div>
|
|
195
|
-
${togglesTpl} ${exportTpl}
|
|
246
|
+
${settingsTpl} ${togglesTpl} ${exportTpl}
|
|
196
247
|
</div>
|
|
197
248
|
`;
|
|
198
249
|
}
|
|
250
|
+
|
|
251
|
+
// ── Dynamic route-param pickers ──────────────────────────────────────────────
|
|
252
|
+
// Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
|
|
253
|
+
// Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
|
|
254
|
+
// Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
|
|
255
|
+
// Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
|
|
256
|
+
// First path entry).
|
|
257
|
+
|
|
258
|
+
let _paramValues: ParamValues | null = null;
|
|
259
|
+
|
|
260
|
+
let _paramValuesKey: string | null = null;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {Tab} tab
|
|
264
|
+
* @returns {ParamValues | null} — null while loading (or when the doc declares no params)
|
|
265
|
+
*/
|
|
266
|
+
function paramValuesFor(tab: Tab): ParamValues | null {
|
|
267
|
+
const pathsDef = pagePathsDef({
|
|
268
|
+
document: tab.doc.document,
|
|
269
|
+
frontmatter: tab.doc.content.frontmatter,
|
|
270
|
+
});
|
|
271
|
+
if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
275
|
+
if (_paramValuesKey === key) {
|
|
276
|
+
return _paramValues;
|
|
277
|
+
}
|
|
278
|
+
_paramValuesKey = key;
|
|
279
|
+
_paramValues = null;
|
|
280
|
+
void loadParamValues(tab.documentPath, pathsDef).then((values) => {
|
|
281
|
+
if (_paramValuesKey !== key || activeTab.value !== tab) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
_paramValues = values;
|
|
285
|
+
autoSelectParams(tab, values);
|
|
286
|
+
render();
|
|
287
|
+
});
|
|
288
|
+
return _paramValues;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {Tab} tab
|
|
293
|
+
* @param {ParamValues} values
|
|
294
|
+
*/
|
|
295
|
+
function autoSelectParams(tab: Tab, values: ParamValues) {
|
|
296
|
+
const current = tab.session.ui.previewParams ?? {};
|
|
297
|
+
const additions: Record<string, string> = {};
|
|
298
|
+
for (const [name, list] of Object.entries(values)) {
|
|
299
|
+
if (!current[name] && list.length > 0) {
|
|
300
|
+
additions[name] = list[0]!;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(additions).length > 0) {
|
|
304
|
+
updateUi("previewParams", { ...current, ...additions });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {Tab} tab
|
|
310
|
+
* @returns {TemplateResult | typeof nothing}
|
|
311
|
+
*/
|
|
312
|
+
function paramPickersTpl(tab: Tab): TemplateResult | typeof nothing {
|
|
313
|
+
const values = paramValuesFor(tab);
|
|
314
|
+
const names = new Set(dynamicRouteParams(tab.documentPath));
|
|
315
|
+
for (const name of Object.keys(values ?? {})) {
|
|
316
|
+
names.add(name);
|
|
317
|
+
}
|
|
318
|
+
if (names.size === 0) {
|
|
319
|
+
return nothing;
|
|
320
|
+
}
|
|
321
|
+
const { previewParams } = tab.session.ui;
|
|
322
|
+
return html`
|
|
323
|
+
${[...names].map(
|
|
324
|
+
(name) => html`
|
|
325
|
+
<sp-picker
|
|
326
|
+
size="s"
|
|
327
|
+
quiet
|
|
328
|
+
class="tab-bar-param"
|
|
329
|
+
label=${name}
|
|
330
|
+
title=${`Preview value for [${name}]`}
|
|
331
|
+
value=${previewParams?.[name] ?? ""}
|
|
332
|
+
@change=${(e: Event) => {
|
|
333
|
+
const { value } = e.target as HTMLInputElement;
|
|
334
|
+
updateUi("previewParams", { ...tab.session.ui.previewParams, [name]: value });
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
${(values?.[name] ?? []).map(
|
|
338
|
+
(v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
|
|
339
|
+
)}
|
|
340
|
+
</sp-picker>
|
|
341
|
+
`,
|
|
342
|
+
)}
|
|
343
|
+
`;
|
|
344
|
+
}
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -305,7 +305,6 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
|
|
|
305
305
|
const modes = [
|
|
306
306
|
{ iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
|
|
307
307
|
{ iconTag: "sp-icon-artboard", key: "design", label: "Design" },
|
|
308
|
-
{ iconTag: "sp-icon-preview", key: "preview", label: "Preview" },
|
|
309
308
|
{ iconTag: "sp-icon-code", key: "source", label: "Code" },
|
|
310
309
|
{ iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
|
|
311
310
|
];
|
|
@@ -333,7 +332,9 @@ function toolbarTemplate() {
|
|
|
333
332
|
selection: tab.session.selection,
|
|
334
333
|
ui: tab.session.ui,
|
|
335
334
|
};
|
|
336
|
-
|
|
335
|
+
// Base mode, not the effective mode: the switcher keeps Edit/Design highlighted while the
|
|
336
|
+
// Tab-bar preview toggle is on (preview is no longer a switchable mode).
|
|
337
|
+
const { canvasMode } = tab.session.ui;
|
|
337
338
|
|
|
338
339
|
const modeSwitcherTpl = html`
|
|
339
340
|
<sp-action-group selects="single" size="s" compact>
|
|
@@ -588,6 +588,21 @@ export function createDevServerPlatform() {
|
|
|
588
588
|
return schema;
|
|
589
589
|
},
|
|
590
590
|
|
|
591
|
+
// ─── Class resolution (dev-proxy) ─────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
/** @param {Record<string, unknown>} body */
|
|
594
|
+
async resolveClass(body: Record<string, unknown>) {
|
|
595
|
+
const res = await fetch("/__jx_resolve__", {
|
|
596
|
+
body: JSON.stringify(body),
|
|
597
|
+
headers: { "content-type": "application/json" },
|
|
598
|
+
method: "POST",
|
|
599
|
+
});
|
|
600
|
+
if (!res.ok) {
|
|
601
|
+
throw new Error(`Class resolution failed: ${res.status}`);
|
|
602
|
+
}
|
|
603
|
+
return await readJson<unknown>(res);
|
|
604
|
+
},
|
|
605
|
+
|
|
591
606
|
// ─── Git operations ──────────────────────────────────────────────────
|
|
592
607
|
|
|
593
608
|
async gitStatus() {
|
|
@@ -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
|
}
|
|
@@ -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
|
|
package/src/studio.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
requireProjectState,
|
|
20
20
|
setProjectState,
|
|
21
21
|
toolbarEl,
|
|
22
|
+
updateSession,
|
|
22
23
|
updateUi,
|
|
23
24
|
} from "./store";
|
|
24
25
|
|
|
@@ -28,20 +29,28 @@ import { effect } from "./reactivity";
|
|
|
28
29
|
|
|
29
30
|
import { view } from "./view";
|
|
30
31
|
|
|
31
|
-
import {
|
|
32
|
-
import { enterComponentInlineEdit, initComponentInlineEdit } from "./editor/component-inline-edit";
|
|
33
|
-
import { enterInlineEdit } from "./editor/content-inline-edit";
|
|
32
|
+
import { isEditing } from "./editor/inline-edit";
|
|
34
33
|
import { applyTransform, initCanvasUtils, positionZoomIndicator } from "./canvas/canvas-utils";
|
|
35
|
-
import { findCanvasElement, getActivePanel, initCanvasHelpers } from "./canvas/canvas-helpers";
|
|
36
34
|
import {
|
|
37
|
-
applyCanvasMediaOverrides,
|
|
38
35
|
initCanvasRender,
|
|
39
36
|
renderCanvas,
|
|
40
37
|
renderOverlays,
|
|
41
38
|
scheduleCanvasRender,
|
|
42
39
|
} from "./canvas/canvas-render";
|
|
43
40
|
import { consumePatchedDocument, initCanvasPatcher } from "./canvas/canvas-patcher";
|
|
44
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
commitActiveEditSession,
|
|
43
|
+
getEditSnapshot,
|
|
44
|
+
setCanvasContextMenuHandler,
|
|
45
|
+
setCanvasSlashHandler,
|
|
46
|
+
setIframePatchEscalation,
|
|
47
|
+
setInsertZoneClickHandler,
|
|
48
|
+
setStylebookHitHandler,
|
|
49
|
+
setToolbarRefresh,
|
|
50
|
+
} from "./canvas/iframe-host";
|
|
51
|
+
import { runInsertZoneAction } from "./editor/insert-zone-action";
|
|
52
|
+
import { canvasSlashHandler } from "./editor/canvas-slash-bridge";
|
|
53
|
+
import { makeCanvasContextMenuHandler } from "./editor/canvas-context-menu";
|
|
45
54
|
import { initCanvasLiveRender } from "./canvas/canvas-live-render";
|
|
46
55
|
import {
|
|
47
56
|
mountStatusbar,
|
|
@@ -50,7 +59,12 @@ import {
|
|
|
50
59
|
statusMessage,
|
|
51
60
|
} from "./panels/statusbar";
|
|
52
61
|
import { exportFile, parseSourceForPath, saveFile, serializeDocument } from "./files/file-ops";
|
|
53
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
documentExtensions,
|
|
64
|
+
formatForPath,
|
|
65
|
+
loadFormats,
|
|
66
|
+
refreshFormats,
|
|
67
|
+
} from "./format/format-host";
|
|
54
68
|
import {
|
|
55
69
|
loadProject as _loadProject,
|
|
56
70
|
openProject as _openProject,
|
|
@@ -97,14 +111,17 @@ import * as rightPanelMod from "./panels/right-panel";
|
|
|
97
111
|
import * as leftPanelMod from "./panels/left-panel";
|
|
98
112
|
import * as tabStrip from "./panels/tab-strip";
|
|
99
113
|
import * as tabBar from "./panels/tab-bar";
|
|
100
|
-
import {
|
|
114
|
+
import { selectStylebookTag } from "./panels/stylebook-panel";
|
|
101
115
|
import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd";
|
|
116
|
+
import { registerCanvasDndBridge } from "./panels/canvas-dnd-bridge";
|
|
102
117
|
import { defaultDef } from "./panels/shared";
|
|
103
118
|
import { registerFunctionCompletions } from "./panels/editors";
|
|
104
|
-
import {
|
|
119
|
+
import {
|
|
120
|
+
initBlockActionBar,
|
|
121
|
+
isEditChromeTarget,
|
|
122
|
+
renderBlockActionBar,
|
|
123
|
+
} from "./panels/block-action-bar";
|
|
105
124
|
import { initCssData } from "./panels/style-utils";
|
|
106
|
-
import { updateForcedPseudoPreview } from "./panels/pseudo-preview";
|
|
107
|
-
import { initPanelEvents } from "./panels/panel-events";
|
|
108
125
|
import { initQuickSearch } from "./panels/quick-search";
|
|
109
126
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
110
127
|
import { initWelcome } from "./panels/welcome-screen";
|
|
@@ -119,8 +136,14 @@ void _swc;
|
|
|
119
136
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
120
137
|
// Into their own modules, they will migrate to ctx in store.js.
|
|
121
138
|
|
|
139
|
+
// Effective canvas mode: the per-tab preview toggle composes with an edit/design base mode and
|
|
140
|
+
// Presents as "preview" to every downstream gate (doc resolution, iframe flags, interaction
|
|
141
|
+
// Surfaces). Consumers needing the base mode (toolbar switcher selection, canvas host layout)
|
|
142
|
+
// Read tab.session.ui.canvasMode directly.
|
|
122
143
|
function getCanvasMode() {
|
|
123
|
-
|
|
144
|
+
const ui = activeTab.value?.session.ui;
|
|
145
|
+
const base = ui?.canvasMode ?? "design";
|
|
146
|
+
return ui?.preview && (base === "edit" || base === "design") ? "preview" : base;
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
/** @param {string} mode */
|
|
@@ -361,39 +384,58 @@ initBlockActionBar({
|
|
|
361
384
|
getCanvasMode,
|
|
362
385
|
navigateToComponent,
|
|
363
386
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
387
|
+
// The iframe's re-emitted selection snapshot drives the parent format toolbar refresh (4b-2).
|
|
388
|
+
setToolbarRefresh(renderBlockActionBar);
|
|
389
|
+
// The cross-origin insertion "+" click runs the parent-realm slash-menu → mutateInsertNode flow.
|
|
390
|
+
setInsertZoneClickHandler(runInsertZoneAction);
|
|
391
|
+
// The in-iframe "/" trigger drives the parent-realm Spectrum slash menu across the bridge.
|
|
392
|
+
setCanvasSlashHandler(canvasSlashHandler);
|
|
393
|
+
// Canvas right-clicks show the parent-realm Jx element context menu across the bridge.
|
|
394
|
+
setCanvasContextMenuHandler(makeCanvasContextMenuHandler({ navigateToComponent }));
|
|
395
|
+
// Stylebook hits decode to a TAG in the host and route here (null = clicked chrome/empty space).
|
|
396
|
+
setStylebookHitHandler((tag, media) => {
|
|
397
|
+
if (tag) {
|
|
398
|
+
selectStylebookTag(tag, media);
|
|
399
|
+
} else {
|
|
400
|
+
updateSession({ ui: { activeSelector: null, stylebookSelection: null } });
|
|
401
|
+
}
|
|
369
402
|
});
|
|
403
|
+
// Commit-on-parent-click: a pointerdown in PARENT chrome outside the edit-session chrome (format
|
|
404
|
+
// Toolbar / link popover / slash menu) ends the live inline-edit session — the iframe can't observe
|
|
405
|
+
// Parent-realm pointer events (layers panel, tab strip, right panel…). Pointerdowns over the canvas
|
|
406
|
+
// Land inside the cross-origin iframe and never reach this listener, so it can't double-fire with
|
|
407
|
+
// The iframe's own click-away commit.
|
|
408
|
+
document.addEventListener(
|
|
409
|
+
"pointerdown",
|
|
410
|
+
(e) => {
|
|
411
|
+
if (getEditSnapshot().editing && !isEditChromeTarget(e.target)) {
|
|
412
|
+
commitActiveEditSession();
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
true,
|
|
416
|
+
);
|
|
417
|
+
|
|
370
418
|
initCanvasUtils({
|
|
371
419
|
getCanvasMode,
|
|
372
420
|
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
373
|
-
renderStylebookOverlays,
|
|
374
421
|
setZoomDirect: (zoom) => {
|
|
375
422
|
if (activeTab.value) {
|
|
376
423
|
activeTab.value.session.ui.zoom = zoom;
|
|
377
424
|
}
|
|
378
425
|
},
|
|
379
426
|
});
|
|
380
|
-
initPanelEvents({
|
|
381
|
-
enterInlineEdit,
|
|
382
|
-
getCanvasMode,
|
|
383
|
-
navigateToComponent,
|
|
384
|
-
});
|
|
385
427
|
initCanvasLiveRender({
|
|
386
428
|
getCanvasMode,
|
|
387
429
|
});
|
|
388
430
|
initCanvasPatcher({
|
|
389
|
-
applyCanvasMediaOverrides,
|
|
390
|
-
enterComponentInlineEdit,
|
|
391
431
|
getCanvasMode,
|
|
392
|
-
registerSubtreeDnD,
|
|
393
432
|
renderOverlays,
|
|
394
433
|
scheduleCanvasRender,
|
|
395
|
-
updateForcedPseudoPreview,
|
|
396
434
|
});
|
|
435
|
+
// When the iframe canvas can't apply a posted patch surgically, fall back to a full render.
|
|
436
|
+
setIframePatchEscalation(scheduleCanvasRender);
|
|
437
|
+
// One global coordinator monitor drives cross-frame palette→canvas drops (Phase 4c).
|
|
438
|
+
registerCanvasDndBridge();
|
|
397
439
|
initCanvasRender({
|
|
398
440
|
getCanvasMode,
|
|
399
441
|
get gitDiffState() {
|
|
@@ -441,7 +483,10 @@ effect(() => {
|
|
|
441
483
|
void tab.session.ui.canvasMode;
|
|
442
484
|
void tab.session.ui.editingFunction;
|
|
443
485
|
void tab.session.ui.featureToggles;
|
|
486
|
+
void tab.session.ui.preview;
|
|
487
|
+
void tab.session.ui.previewParams;
|
|
444
488
|
void tab.session.ui.settingsTab;
|
|
489
|
+
void tab.session.ui.showLayout;
|
|
445
490
|
void tab.session.ui.stylebookTab;
|
|
446
491
|
void tab.session.ui.stylebookFilter;
|
|
447
492
|
void tab.session.ui.stylebookCustomizedOnly;
|
|
@@ -453,7 +498,6 @@ rightPanelMod.mount({
|
|
|
453
498
|
getCanvasMode,
|
|
454
499
|
navigateToComponent,
|
|
455
500
|
renderCanvas: () => renderCanvas(),
|
|
456
|
-
updateForcedPseudoPreview,
|
|
457
501
|
});
|
|
458
502
|
|
|
459
503
|
leftPanelMod.mount({
|
|
@@ -520,6 +564,19 @@ function ensureFsSync() {
|
|
|
520
564
|
const _urlParams = new URLSearchParams(location.search);
|
|
521
565
|
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
522
566
|
|
|
567
|
+
if (!_projectParam) {
|
|
568
|
+
// Electrobun (and other non-?project= hosts) load their project over RPC, so the ?project= branch
|
|
569
|
+
// Below — which is the only place that calls platform.activate() — is skipped. Kick off activate()
|
|
570
|
+
// Here UNCONDITIONALLY so this window's loopback canvasUrl is fetched on boot (the canvas iframe
|
|
571
|
+
// Needs it); a render() once it resolves lets ensureHost swap an early default iframe for the
|
|
572
|
+
// Loopback one (see iframe-host ensureHost's canvasUrl-changed rebuild).
|
|
573
|
+
const _bootPlatform = getPlatform();
|
|
574
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- fire-and-forget: must not block the initial render
|
|
575
|
+
void _bootPlatform.activate?.()?.then(() => {
|
|
576
|
+
render();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
523
580
|
if (_projectParam) {
|
|
524
581
|
// ?project= mode: skip normal loadProject, set up site context from the path
|
|
525
582
|
const isAbsPath =
|
|
@@ -708,6 +765,11 @@ async function openRecentProject(root: string) {
|
|
|
708
765
|
}
|
|
709
766
|
|
|
710
767
|
platform.projectRoot = root;
|
|
768
|
+
// The format registry is cached per project — the previous root's registry (often empty on a
|
|
769
|
+
// Fresh desktop launch) must not answer for this project, or non-JSON documents fail with
|
|
770
|
+
// "No format class imported" until a reload. Mirrors openProject in files.ts.
|
|
771
|
+
refreshFormats();
|
|
772
|
+
void loadFormats();
|
|
711
773
|
const content = await platform.readFile("project.json");
|
|
712
774
|
const config = JSON.parse(content) as ProjectConfig;
|
|
713
775
|
|
|
@@ -779,18 +841,6 @@ function openFileFromTree(path: string) {
|
|
|
779
841
|
initShortcuts(() => ({
|
|
780
842
|
applyTransform,
|
|
781
843
|
canvasMode: getCanvasMode(),
|
|
782
|
-
componentInlineEdit: view.componentInlineEdit,
|
|
783
|
-
enterEditOnPath(path) {
|
|
784
|
-
requestAnimationFrame(() => {
|
|
785
|
-
const activePanel = getActivePanel();
|
|
786
|
-
if (activePanel) {
|
|
787
|
-
const el = findCanvasElement(path, activePanel.canvas);
|
|
788
|
-
if (el && isEditableBlock(el)) {
|
|
789
|
-
enterInlineEdit(el, path);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
});
|
|
793
|
-
},
|
|
794
844
|
openProject,
|
|
795
845
|
panX: view.panX,
|
|
796
846
|
panY: view.panY,
|