@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
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,
|
|
@@ -66,6 +80,8 @@ import { startFsSync } from "./files/fs-events";
|
|
|
66
80
|
import { renderImportsTemplate } from "./panels/imports-panel";
|
|
67
81
|
import { renderHeadTemplate } from "./panels/head-panel";
|
|
68
82
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
|
|
83
|
+
import { installAutomationHook } from "./services/automation";
|
|
84
|
+
import { openBrowseModal } from "./browse/browse-modal";
|
|
69
85
|
|
|
70
86
|
import { getPlatform, hasPlatform, registerPlatform } from "./platform";
|
|
71
87
|
import { parseMediaEntries } from "./utils/canvas-media";
|
|
@@ -76,6 +92,7 @@ import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/sign
|
|
|
76
92
|
import { loadComponentRegistry } from "./files/components";
|
|
77
93
|
import { ensureDependenciesInstalled } from "./packages/ensure-deps";
|
|
78
94
|
import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
|
|
95
|
+
import { autoSyncProjectOnOpen } from "./packages/pull-package-sync";
|
|
79
96
|
|
|
80
97
|
import { html, render as litRender } from "lit-html";
|
|
81
98
|
|
|
@@ -97,16 +114,20 @@ import * as rightPanelMod from "./panels/right-panel";
|
|
|
97
114
|
import * as leftPanelMod from "./panels/left-panel";
|
|
98
115
|
import * as tabStrip from "./panels/tab-strip";
|
|
99
116
|
import * as tabBar from "./panels/tab-bar";
|
|
100
|
-
import {
|
|
117
|
+
import { selectStylebookTag } from "./panels/stylebook-panel";
|
|
101
118
|
import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd";
|
|
119
|
+
import { registerCanvasDndBridge } from "./panels/canvas-dnd-bridge";
|
|
102
120
|
import { defaultDef } from "./panels/shared";
|
|
103
121
|
import { registerFunctionCompletions } from "./panels/editors";
|
|
104
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
initBlockActionBar,
|
|
124
|
+
isEditChromeTarget,
|
|
125
|
+
renderBlockActionBar,
|
|
126
|
+
} from "./panels/block-action-bar";
|
|
105
127
|
import { initCssData } from "./panels/style-utils";
|
|
106
|
-
import { updateForcedPseudoPreview } from "./panels/pseudo-preview";
|
|
107
|
-
import { initPanelEvents } from "./panels/panel-events";
|
|
108
128
|
import { initQuickSearch } from "./panels/quick-search";
|
|
109
129
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
130
|
+
import { hydrateSettings } from "./services/settings-store";
|
|
110
131
|
import { initWelcome } from "./panels/welcome-screen";
|
|
111
132
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
112
133
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
@@ -119,8 +140,14 @@ void _swc;
|
|
|
119
140
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
120
141
|
// Into their own modules, they will migrate to ctx in store.js.
|
|
121
142
|
|
|
143
|
+
// Effective canvas mode: the per-tab preview toggle composes with an edit/design base mode and
|
|
144
|
+
// Presents as "preview" to every downstream gate (doc resolution, iframe flags, interaction
|
|
145
|
+
// Surfaces). Consumers needing the base mode (toolbar switcher selection, canvas host layout)
|
|
146
|
+
// Read tab.session.ui.canvasMode directly.
|
|
122
147
|
function getCanvasMode() {
|
|
123
|
-
|
|
148
|
+
const ui = activeTab.value?.session.ui;
|
|
149
|
+
const base = ui?.canvasMode ?? "design";
|
|
150
|
+
return ui?.preview && (base === "edit" || base === "design") ? "preview" : base;
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
/** @param {string} mode */
|
|
@@ -319,6 +346,18 @@ if (!hasPlatform()) {
|
|
|
319
346
|
registerPlatform(createDevServerPlatform());
|
|
320
347
|
}
|
|
321
348
|
|
|
349
|
+
// Screenshot/automation runners (scripts/screenshots/) await window.__jxAutomation right after
|
|
350
|
+
// Navigation, so the gated hook must install before the async deep-link project load below.
|
|
351
|
+
installAutomationHook({
|
|
352
|
+
getCanvasMode,
|
|
353
|
+
openBrowseModal,
|
|
354
|
+
openNewProjectModal,
|
|
355
|
+
render,
|
|
356
|
+
renderActivityBar,
|
|
357
|
+
setCanvasMode,
|
|
358
|
+
statusMessage,
|
|
359
|
+
});
|
|
360
|
+
|
|
322
361
|
mountResizeEdges();
|
|
323
362
|
|
|
324
363
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
@@ -361,39 +400,58 @@ initBlockActionBar({
|
|
|
361
400
|
getCanvasMode,
|
|
362
401
|
navigateToComponent,
|
|
363
402
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
403
|
+
// The iframe's re-emitted selection snapshot drives the parent format toolbar refresh (4b-2).
|
|
404
|
+
setToolbarRefresh(renderBlockActionBar);
|
|
405
|
+
// The cross-origin insertion "+" click runs the parent-realm slash-menu → mutateInsertNode flow.
|
|
406
|
+
setInsertZoneClickHandler(runInsertZoneAction);
|
|
407
|
+
// The in-iframe "/" trigger drives the parent-realm Spectrum slash menu across the bridge.
|
|
408
|
+
setCanvasSlashHandler(canvasSlashHandler);
|
|
409
|
+
// Canvas right-clicks show the parent-realm Jx element context menu across the bridge.
|
|
410
|
+
setCanvasContextMenuHandler(makeCanvasContextMenuHandler({ navigateToComponent }));
|
|
411
|
+
// Stylebook hits decode to a TAG in the host and route here (null = clicked chrome/empty space).
|
|
412
|
+
setStylebookHitHandler((tag, media) => {
|
|
413
|
+
if (tag) {
|
|
414
|
+
selectStylebookTag(tag, media);
|
|
415
|
+
} else {
|
|
416
|
+
updateSession({ ui: { activeSelector: null, stylebookSelection: null } });
|
|
417
|
+
}
|
|
369
418
|
});
|
|
419
|
+
// Commit-on-parent-click: a pointerdown in PARENT chrome outside the edit-session chrome (format
|
|
420
|
+
// Toolbar / link popover / slash menu) ends the live inline-edit session — the iframe can't observe
|
|
421
|
+
// Parent-realm pointer events (layers panel, tab strip, right panel…). Pointerdowns over the canvas
|
|
422
|
+
// Land inside the cross-origin iframe and never reach this listener, so it can't double-fire with
|
|
423
|
+
// The iframe's own click-away commit.
|
|
424
|
+
document.addEventListener(
|
|
425
|
+
"pointerdown",
|
|
426
|
+
(e) => {
|
|
427
|
+
if (getEditSnapshot().editing && !isEditChromeTarget(e.target)) {
|
|
428
|
+
commitActiveEditSession();
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
true,
|
|
432
|
+
);
|
|
433
|
+
|
|
370
434
|
initCanvasUtils({
|
|
371
435
|
getCanvasMode,
|
|
372
436
|
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
373
|
-
renderStylebookOverlays,
|
|
374
437
|
setZoomDirect: (zoom) => {
|
|
375
438
|
if (activeTab.value) {
|
|
376
439
|
activeTab.value.session.ui.zoom = zoom;
|
|
377
440
|
}
|
|
378
441
|
},
|
|
379
442
|
});
|
|
380
|
-
initPanelEvents({
|
|
381
|
-
enterInlineEdit,
|
|
382
|
-
getCanvasMode,
|
|
383
|
-
navigateToComponent,
|
|
384
|
-
});
|
|
385
443
|
initCanvasLiveRender({
|
|
386
444
|
getCanvasMode,
|
|
387
445
|
});
|
|
388
446
|
initCanvasPatcher({
|
|
389
|
-
applyCanvasMediaOverrides,
|
|
390
|
-
enterComponentInlineEdit,
|
|
391
447
|
getCanvasMode,
|
|
392
|
-
registerSubtreeDnD,
|
|
393
448
|
renderOverlays,
|
|
394
449
|
scheduleCanvasRender,
|
|
395
|
-
updateForcedPseudoPreview,
|
|
396
450
|
});
|
|
451
|
+
// When the iframe canvas can't apply a posted patch surgically, fall back to a full render.
|
|
452
|
+
setIframePatchEscalation(scheduleCanvasRender);
|
|
453
|
+
// One global coordinator monitor drives cross-frame palette→canvas drops (Phase 4c).
|
|
454
|
+
registerCanvasDndBridge();
|
|
397
455
|
initCanvasRender({
|
|
398
456
|
getCanvasMode,
|
|
399
457
|
get gitDiffState() {
|
|
@@ -441,7 +499,10 @@ effect(() => {
|
|
|
441
499
|
void tab.session.ui.canvasMode;
|
|
442
500
|
void tab.session.ui.editingFunction;
|
|
443
501
|
void tab.session.ui.featureToggles;
|
|
502
|
+
void tab.session.ui.preview;
|
|
503
|
+
void tab.session.ui.previewParams;
|
|
444
504
|
void tab.session.ui.settingsTab;
|
|
505
|
+
void tab.session.ui.showLayout;
|
|
445
506
|
void tab.session.ui.stylebookTab;
|
|
446
507
|
void tab.session.ui.stylebookFilter;
|
|
447
508
|
void tab.session.ui.stylebookCustomizedOnly;
|
|
@@ -453,7 +514,6 @@ rightPanelMod.mount({
|
|
|
453
514
|
getCanvasMode,
|
|
454
515
|
navigateToComponent,
|
|
455
516
|
renderCanvas: () => renderCanvas(),
|
|
456
|
-
updateForcedPseudoPreview,
|
|
457
517
|
});
|
|
458
518
|
|
|
459
519
|
leftPanelMod.mount({
|
|
@@ -520,6 +580,19 @@ function ensureFsSync() {
|
|
|
520
580
|
const _urlParams = new URLSearchParams(location.search);
|
|
521
581
|
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
522
582
|
|
|
583
|
+
if (!_projectParam) {
|
|
584
|
+
// Electrobun (and other non-?project= hosts) load their project over RPC, so the ?project= branch
|
|
585
|
+
// Below — which is the only place that calls platform.activate() — is skipped. Kick off activate()
|
|
586
|
+
// Here UNCONDITIONALLY so this window's loopback canvasUrl is fetched on boot (the canvas iframe
|
|
587
|
+
// Needs it); a render() once it resolves lets ensureHost swap an early default iframe for the
|
|
588
|
+
// Loopback one (see iframe-host ensureHost's canvasUrl-changed rebuild).
|
|
589
|
+
const _bootPlatform = getPlatform();
|
|
590
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- fire-and-forget: must not block the initial render
|
|
591
|
+
void _bootPlatform.activate?.()?.then(() => {
|
|
592
|
+
render();
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
523
596
|
if (_projectParam) {
|
|
524
597
|
// ?project= mode: skip normal loadProject, set up site context from the path
|
|
525
598
|
const isAbsPath =
|
|
@@ -562,6 +635,7 @@ if (_projectParam) {
|
|
|
562
635
|
selectedPath: siteCtx.fileRelPath || null,
|
|
563
636
|
});
|
|
564
637
|
|
|
638
|
+
await autoSyncProjectOnOpen();
|
|
565
639
|
await ensureDependenciesInstalled();
|
|
566
640
|
await loadComponentRegistry();
|
|
567
641
|
|
|
@@ -670,6 +744,13 @@ void hydrateRecentProjects().then(() => {
|
|
|
670
744
|
render();
|
|
671
745
|
});
|
|
672
746
|
|
|
747
|
+
// Hydrate user settings (AI connection parameters) from the backend store, then re-render so
|
|
748
|
+
// Key-gated surfaces (assistant gate, New Project Import/Agent tabs) see the stored key.
|
|
749
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
|
|
750
|
+
void hydrateSettings().then(() => {
|
|
751
|
+
render();
|
|
752
|
+
});
|
|
753
|
+
|
|
673
754
|
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
674
755
|
|
|
675
756
|
function renderLeftPanel() {
|
|
@@ -708,6 +789,11 @@ async function openRecentProject(root: string) {
|
|
|
708
789
|
}
|
|
709
790
|
|
|
710
791
|
platform.projectRoot = root;
|
|
792
|
+
// The format registry is cached per project — the previous root's registry (often empty on a
|
|
793
|
+
// Fresh desktop launch) must not answer for this project, or non-JSON documents fail with
|
|
794
|
+
// "No format class imported" until a reload. Mirrors openProject in files.ts.
|
|
795
|
+
refreshFormats();
|
|
796
|
+
void loadFormats();
|
|
711
797
|
const content = await platform.readFile("project.json");
|
|
712
798
|
const config = JSON.parse(content) as ProjectConfig;
|
|
713
799
|
|
|
@@ -725,6 +811,7 @@ async function openRecentProject(root: string) {
|
|
|
725
811
|
selectedPath: null,
|
|
726
812
|
});
|
|
727
813
|
|
|
814
|
+
await autoSyncProjectOnOpen();
|
|
728
815
|
await ensureDependenciesInstalled();
|
|
729
816
|
await loadDirectory(".");
|
|
730
817
|
await loadComponentRegistry();
|
|
@@ -779,18 +866,6 @@ function openFileFromTree(path: string) {
|
|
|
779
866
|
initShortcuts(() => ({
|
|
780
867
|
applyTransform,
|
|
781
868
|
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
869
|
openProject,
|
|
795
870
|
panX: view.panX,
|
|
796
871
|
panY: view.panY,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure document-op applier — folds a value-carrying {@link JxDocOp} into a bare (non-reactive)
|
|
3
|
+
* document tree. Shared by two consumers that MUST stay byte-identical or undo/redo would drift
|
|
4
|
+
* from live editing: history replay in the parent ({@link file://./transact.ts}) and the iframe
|
|
5
|
+
* canvas's non-reactive shadow doc (the patch source-of-truth across the cross-origin bridge).
|
|
6
|
+
*
|
|
7
|
+
* Dependency-light on purpose: it pulls only the pure path helper from `../state` and clones values
|
|
8
|
+
* with a local JSON round-trip, so the slim canvas-iframe bundle can import it without dragging in
|
|
9
|
+
* the editor's reactive store / format host.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getNodeAtPath } from "../state";
|
|
13
|
+
import type { JxDocOp } from "./patch-ops";
|
|
14
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
15
|
+
|
|
16
|
+
/** JSON round-trip clone — also normalizes away reactive proxies / functions / undefined. */
|
|
17
|
+
function jsonClone<T>(value: T): T {
|
|
18
|
+
// oxlint-disable-next-line unicorn/prefer-structured-clone -- structuredClone throws on reactive proxies; JSON normalization is the point
|
|
19
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Deep-clone a recorded value (undefined/null pass through; reactive proxies are read through). */
|
|
23
|
+
export function cloneValue<T>(v: T): T {
|
|
24
|
+
return v === undefined || v === null ? v : (jsonClone(v as object) as T);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The node's children array, lazily created; throws if `node` is itself a children array or mapped. */
|
|
28
|
+
export function childArray(node: JxMutableNode): (JxMutableNode | string)[] {
|
|
29
|
+
// Defense-in-depth: a path that resolves to a children array (rather than a node) would
|
|
30
|
+
// Otherwise get a bogus `.children` property tacked on here, silently storing the insert where
|
|
31
|
+
// Nothing renders. Callers must pass a node; fail loudly if they don't.
|
|
32
|
+
if (Array.isArray(node)) {
|
|
33
|
+
throw new TypeError("Cannot insert into a children array; parentPath must point at a node");
|
|
34
|
+
}
|
|
35
|
+
if (!node.children) {
|
|
36
|
+
node.children = [];
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(node.children)) {
|
|
39
|
+
throw new TypeError("Cannot insert into mapped-array children; edit the map template instead");
|
|
40
|
+
}
|
|
41
|
+
return node.children;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply a replayable doc op to a bare document tree. Values/nodes are cloned in, so the tree never
|
|
46
|
+
* aliases the op object. Throws (with a machine-readable reason) when a target path is missing.
|
|
47
|
+
*/
|
|
48
|
+
export function applyDocOpToDoc(doc: JxMutableNode, op: JxDocOp): void {
|
|
49
|
+
switch (op.op) {
|
|
50
|
+
case "set-key": {
|
|
51
|
+
const node = getNodeAtPath(doc, op.path);
|
|
52
|
+
if (!node) {
|
|
53
|
+
throw new Error(`doc-op-node-not-found:${op.path.join("/")}`);
|
|
54
|
+
}
|
|
55
|
+
const target = node as Record<string, unknown>;
|
|
56
|
+
if (op.value === undefined) {
|
|
57
|
+
delete target[op.key];
|
|
58
|
+
} else {
|
|
59
|
+
target[op.key] = cloneValue(op.value);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case "insert-child": {
|
|
64
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
65
|
+
childArray(parent).splice(op.index, 0, cloneValue(op.node) as JxMutableNode);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
case "remove-child": {
|
|
69
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
70
|
+
childArray(parent).splice(op.index, 1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case "set-child": {
|
|
74
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
75
|
+
childArray(parent).splice(op.index, 1, cloneValue(op.node) as JxMutableNode);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "move-child": {
|
|
79
|
+
const fromParent = getNodeAtPath(doc, op.fromParentPath);
|
|
80
|
+
const toParent = getNodeAtPath(doc, op.toParentPath);
|
|
81
|
+
const [node] = childArray(fromParent).splice(op.fromIndex, 1);
|
|
82
|
+
childArray(toParent).splice(op.toIndex, 0, node!);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
throw new Error(`unknown-doc-op:${(op as JxDocOp).op}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/tabs/patch-ops.ts
CHANGED
|
@@ -78,8 +78,12 @@ export interface PatchConsumer {
|
|
|
78
78
|
classify: (tab: Tab, ops: JxPatchOp[]) => { patchable: boolean; reason: string };
|
|
79
79
|
/** Mark a document root reference as surgically consumed (checked by the canvas doc-effect). */
|
|
80
80
|
markConsumed: (docRef: object) => void;
|
|
81
|
-
/**
|
|
82
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Apply the batch to all ready canvas panels. Throws on any failure (caller escalates). `record`
|
|
83
|
+
* carries the value-carrying `docOps` the iframe consumer posts across the frame boundary; the
|
|
84
|
+
* legacy (parent-DOM) consumer ignores it and reads the post-mutation reactive doc instead.
|
|
85
|
+
*/
|
|
86
|
+
apply: (tab: Tab, ops: JxPatchOp[], record?: TransactionRecord) => void;
|
|
83
87
|
/** Schedule a full canvas render as the fallback path, recording the reason. */
|
|
84
88
|
escalate: (reason: string) => void;
|
|
85
89
|
}
|
package/src/tabs/tab.ts
CHANGED
|
@@ -16,6 +16,12 @@ import type { JxDocOp } from "./patch-ops";
|
|
|
16
16
|
export interface TabUi {
|
|
17
17
|
rightTab: string;
|
|
18
18
|
canvasMode: string;
|
|
19
|
+
/** Preview toggle — composes with an edit/design canvasMode; the effective mode becomes "preview". */
|
|
20
|
+
preview: boolean;
|
|
21
|
+
/** Show elements inherited from the page's layout (pages with an effective layout only). */
|
|
22
|
+
showLayout: boolean;
|
|
23
|
+
/** Chosen literal values for dynamic route params (e.g. { sku: "mini-trencher" }). */
|
|
24
|
+
previewParams: Record<string, string>;
|
|
19
25
|
zoom: number;
|
|
20
26
|
activeMedia: string | null;
|
|
21
27
|
activeSelector: string | null;
|
|
@@ -79,7 +85,10 @@ export interface Tab {
|
|
|
79
85
|
ui: TabUi;
|
|
80
86
|
canvas: {
|
|
81
87
|
status: string;
|
|
82
|
-
|
|
88
|
+
// A serializable snapshot of the iframe's resolved `$defs` (data-source values), posted over
|
|
89
|
+
// The bridge as a `dataScope` message and read by the data-explorer panel. Plain data now —
|
|
90
|
+
// The old live `EffectScope` (with `.stop()`) moved into the iframe realm with buildScope.
|
|
91
|
+
scope: Record<string, unknown> | null;
|
|
83
92
|
error: string | null;
|
|
84
93
|
pendingInlineEdit: InlineEditDef | null;
|
|
85
94
|
};
|
|
@@ -92,9 +101,10 @@ export interface Tab {
|
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
103
|
* @param {string} canvasMode — initial canvas mode (the tab's first allowed mode)
|
|
104
|
+
* @param {boolean} preview — initial preview-toggle state
|
|
95
105
|
* @returns {TabUi}
|
|
96
106
|
*/
|
|
97
|
-
function createDefaultUi(canvasMode: string) {
|
|
107
|
+
function createDefaultUi(canvasMode: string, preview = false) {
|
|
98
108
|
return {
|
|
99
109
|
activeMedia: null,
|
|
100
110
|
activeSelector: null,
|
|
@@ -109,8 +119,11 @@ function createDefaultUi(canvasMode: string) {
|
|
|
109
119
|
gitStatus: null,
|
|
110
120
|
inspectorSections: {},
|
|
111
121
|
pendingInlineEdit: null,
|
|
122
|
+
preview,
|
|
123
|
+
previewParams: {},
|
|
112
124
|
rightTab: "properties",
|
|
113
125
|
settingsTab: "stylebook",
|
|
126
|
+
showLayout: true,
|
|
114
127
|
styleFilter: "",
|
|
115
128
|
styleFilterActive: false,
|
|
116
129
|
styleSections: {},
|
|
@@ -165,7 +178,10 @@ export function createTab({
|
|
|
165
178
|
const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
|
|
166
179
|
// A tab opens in its first allowed mode — never one the toolbar would disable.
|
|
167
180
|
// Formats author mode order so the default comes first (edit, stylebook, etc.).
|
|
168
|
-
|
|
181
|
+
// "preview" is a per-tab toggle rather than a base mode: a preview-first format opens
|
|
182
|
+
// In its first non-preview mode with the toggle already on.
|
|
183
|
+
const initialCanvasMode = resolvedModes.find((m) => m !== "preview") ?? "edit";
|
|
184
|
+
const initialPreview = resolvedModes[0] === "preview";
|
|
169
185
|
|
|
170
186
|
const tab = scope.run(() => ({
|
|
171
187
|
capabilities: { modes: resolvedModes },
|
|
@@ -196,7 +212,7 @@ export function createTab({
|
|
|
196
212
|
documentStack: [],
|
|
197
213
|
hover: null,
|
|
198
214
|
selection: null,
|
|
199
|
-
ui: createDefaultUi(initialCanvasMode),
|
|
215
|
+
ui: createDefaultUi(initialCanvasMode, initialPreview),
|
|
200
216
|
}),
|
|
201
217
|
})) as unknown as Tab;
|
|
202
218
|
|
|
@@ -204,6 +220,9 @@ export function createTab({
|
|
|
204
220
|
}
|
|
205
221
|
|
|
206
222
|
/**
|
|
223
|
+
* Allowed modes for a document. "preview" in a format's mode list means the preview toggle is
|
|
224
|
+
* Available for the tab (it is not a base canvas mode the toolbar switches to).
|
|
225
|
+
*
|
|
207
226
|
* @param {string | null | undefined} documentPath
|
|
208
227
|
* @param {string | null} sourceFormat
|
|
209
228
|
* @returns {string[]}
|
package/src/tabs/transact.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { toRaw } from "../reactivity";
|
|
3
3
|
import { jsonClone } from "../utils/studio-utils";
|
|
4
4
|
import { childIndex, getNodeAtPath, isAncestor, parentElementPath, pathsEqual } from "../state";
|
|
5
|
+
import { applyDocOpToDoc, childArray, cloneValue } from "./doc-op-apply";
|
|
5
6
|
import {
|
|
6
7
|
beginRecording,
|
|
7
8
|
endRecording,
|
|
@@ -33,11 +34,6 @@ function patchHistoryEnabled() {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** Deep-clone a recorded value (undefined passes through; reactive proxies are read through). */
|
|
37
|
-
function cloneValue<T>(v: T): T {
|
|
38
|
-
return v === undefined || v === null ? v : (jsonClone(v as object) as T);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
/** Forward/inverse pair for a single-key change on the node at path. */
|
|
42
38
|
function setKeyPair(path: JxPath, key: string, before: unknown, after: unknown): JxDocOpPair {
|
|
43
39
|
return {
|
|
@@ -81,26 +77,6 @@ export type JxNodeValue =
|
|
|
81
77
|
| JxEventBinding
|
|
82
78
|
| undefined;
|
|
83
79
|
|
|
84
|
-
/**
|
|
85
|
-
* The editable children array of a node, created when absent. Mapped-array children (`$prototype:
|
|
86
|
-
* "Array"`) cannot be index-mutated — fail loudly instead of corrupting.
|
|
87
|
-
*/
|
|
88
|
-
function childArray(node: JxMutableNode): (JxMutableNode | string)[] {
|
|
89
|
-
// Defense-in-depth: a path that resolves to a children array (rather than a node) would
|
|
90
|
-
// Otherwise get a bogus `.children` property tacked on here, silently storing the insert where
|
|
91
|
-
// Nothing renders. Callers must pass a node; fail loudly if they don't.
|
|
92
|
-
if (Array.isArray(node)) {
|
|
93
|
-
throw new TypeError("Cannot insert into a children array; parentPath must point at a node");
|
|
94
|
-
}
|
|
95
|
-
if (!node.children) {
|
|
96
|
-
node.children = [];
|
|
97
|
-
}
|
|
98
|
-
if (!Array.isArray(node.children)) {
|
|
99
|
-
throw new TypeError("Cannot insert into mapped-array children; edit the map template instead");
|
|
100
|
-
}
|
|
101
|
-
return node.children;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
80
|
// ─── Transactional layer ─────────────────────────────────────────────────────
|
|
105
81
|
|
|
106
82
|
/**
|
|
@@ -152,7 +128,7 @@ export function transactDoc(
|
|
|
152
128
|
|
|
153
129
|
if (verdict.patchable) {
|
|
154
130
|
try {
|
|
155
|
-
consumer!.apply(tab, record.ops);
|
|
131
|
+
consumer!.apply(tab, record.ops, record);
|
|
156
132
|
} catch (error) {
|
|
157
133
|
consumer!.escalate(
|
|
158
134
|
`patch-apply-failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -254,54 +230,6 @@ export function transact(
|
|
|
254
230
|
/** Document-level keys whose changes require a full scope/panel rebuild on the canvas. */
|
|
255
231
|
const DOC_META_KEYS = new Set(["state", "$media", "$head", "$elements", "imports", "$layout"]);
|
|
256
232
|
|
|
257
|
-
/**
|
|
258
|
-
* Apply a replayable doc op to a bare document tree (history replay — no canvas recording).
|
|
259
|
-
*
|
|
260
|
-
* @param {JxMutableNode} doc
|
|
261
|
-
* @param {JxDocOp} op
|
|
262
|
-
*/
|
|
263
|
-
function applyDocOpToDoc(doc: JxMutableNode, op: JxDocOp) {
|
|
264
|
-
switch (op.op) {
|
|
265
|
-
case "set-key": {
|
|
266
|
-
const node = getNodeAtPath(doc, op.path);
|
|
267
|
-
if (!node) {
|
|
268
|
-
throw new Error(`doc-op-node-not-found:${op.path.join("/")}`);
|
|
269
|
-
}
|
|
270
|
-
if (op.value === undefined) {
|
|
271
|
-
delete node[op.key];
|
|
272
|
-
} else {
|
|
273
|
-
node[op.key] = cloneValue(op.value) as JxNodeValue;
|
|
274
|
-
}
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
case "insert-child": {
|
|
278
|
-
const parent = getNodeAtPath(doc, op.parentPath);
|
|
279
|
-
childArray(parent).splice(op.index, 0, cloneValue(op.node) as JxMutableNode);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
case "remove-child": {
|
|
283
|
-
const parent = getNodeAtPath(doc, op.parentPath);
|
|
284
|
-
childArray(parent).splice(op.index, 1);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
case "set-child": {
|
|
288
|
-
const parent = getNodeAtPath(doc, op.parentPath);
|
|
289
|
-
childArray(parent).splice(op.index, 1, cloneValue(op.node) as JxMutableNode);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
case "move-child": {
|
|
293
|
-
const fromParent = getNodeAtPath(doc, op.fromParentPath);
|
|
294
|
-
const toParent = getNodeAtPath(doc, op.toParentPath);
|
|
295
|
-
const [node] = childArray(fromParent).splice(op.fromIndex, 1);
|
|
296
|
-
childArray(toParent).splice(op.toIndex, 0, node!);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
default: {
|
|
300
|
-
throw new Error(`unknown-doc-op:${(op as JxDocOp).op}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
233
|
/**
|
|
306
234
|
* Apply a doc op to the live document AND record the matching canvas patch op, so undo/redo
|
|
307
235
|
* transactions patch the canvas surgically like any other edit.
|