@jxsuite/studio 0.28.5 → 0.30.1
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/studio.css +98 -98
- package/dist/studio.js +9368 -6586
- package/dist/studio.js.map +109 -83
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +11 -10
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +96 -76
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +68 -24
- package/src/files/fs-events.ts +162 -0
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +14 -17
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +43 -33
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +113 -35
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +88 -35
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +71 -1
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +14 -8
- package/src/ui/unit-selector.ts +5 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- package/src/view.ts +4 -1
package/src/studio.ts
CHANGED
|
@@ -29,11 +29,19 @@ import { effect } from "./reactivity";
|
|
|
29
29
|
import { view } from "./view";
|
|
30
30
|
|
|
31
31
|
import { isEditableBlock, isEditing } from "./editor/inline-edit";
|
|
32
|
-
import { initComponentInlineEdit } from "./editor/component-inline-edit";
|
|
32
|
+
import { enterComponentInlineEdit, initComponentInlineEdit } from "./editor/component-inline-edit";
|
|
33
33
|
import { enterInlineEdit } from "./editor/content-inline-edit";
|
|
34
34
|
import { applyTransform, initCanvasUtils, positionZoomIndicator } from "./canvas/canvas-utils";
|
|
35
35
|
import { findCanvasElement, getActivePanel, initCanvasHelpers } from "./canvas/canvas-helpers";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
applyCanvasMediaOverrides,
|
|
38
|
+
initCanvasRender,
|
|
39
|
+
renderCanvas,
|
|
40
|
+
renderOverlays,
|
|
41
|
+
scheduleCanvasRender,
|
|
42
|
+
} from "./canvas/canvas-render";
|
|
43
|
+
import { consumePatchedDocument, initCanvasPatcher } from "./canvas/canvas-patcher";
|
|
44
|
+
import { registerSubtreeDnD } from "./panels/canvas-dnd";
|
|
37
45
|
import { initCanvasLiveRender } from "./canvas/canvas-live-render";
|
|
38
46
|
import {
|
|
39
47
|
mountStatusbar,
|
|
@@ -51,8 +59,10 @@ import {
|
|
|
51
59
|
openFileInTab,
|
|
52
60
|
openHomePage,
|
|
53
61
|
registerFileTreeDnD,
|
|
62
|
+
reloadCleanTab,
|
|
54
63
|
setupTreeKeyboard,
|
|
55
64
|
} from "./files/files";
|
|
65
|
+
import { startFsSync } from "./files/fs-events";
|
|
56
66
|
import { renderImportsTemplate } from "./panels/imports-panel";
|
|
57
67
|
import { renderHeadTemplate } from "./panels/head-panel";
|
|
58
68
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
|
|
@@ -84,6 +94,7 @@ import * as overlaysPanel from "./panels/overlays";
|
|
|
84
94
|
import * as rightPanelMod from "./panels/right-panel";
|
|
85
95
|
import * as leftPanelMod from "./panels/left-panel";
|
|
86
96
|
import * as tabStrip from "./panels/tab-strip";
|
|
97
|
+
import * as tabBar from "./panels/tab-bar";
|
|
87
98
|
import { renderStylebookOverlays } from "./panels/stylebook-panel";
|
|
88
99
|
import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd";
|
|
89
100
|
import { defaultDef } from "./panels/shared";
|
|
@@ -98,7 +109,7 @@ import { initWelcome } from "./panels/welcome-screen";
|
|
|
98
109
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
99
110
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
100
111
|
import type { JxPath } from "./state";
|
|
101
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
112
|
+
import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
102
113
|
|
|
103
114
|
void _swc;
|
|
104
115
|
|
|
@@ -133,7 +144,7 @@ async function navigateToComponent(componentPath: string) {
|
|
|
133
144
|
if (!content) {
|
|
134
145
|
return;
|
|
135
146
|
}
|
|
136
|
-
const parsed = JSON.parse(content);
|
|
147
|
+
const parsed = JSON.parse(content) as JxMutableNode;
|
|
137
148
|
const tab = activeTab.value;
|
|
138
149
|
if (!tab) {
|
|
139
150
|
return;
|
|
@@ -289,7 +300,7 @@ requestIdleCallback(() => {
|
|
|
289
300
|
const frag = document.createDocumentFragment();
|
|
290
301
|
for (const [name] of webdata.cssProps) {
|
|
291
302
|
const opt = document.createElement("option");
|
|
292
|
-
opt.value = name
|
|
303
|
+
opt.value = name!;
|
|
293
304
|
frag.append(opt);
|
|
294
305
|
}
|
|
295
306
|
dl.append(frag);
|
|
@@ -316,11 +327,8 @@ initShellRefs();
|
|
|
316
327
|
toolbarPanel.mount(toolbarEl, {
|
|
317
328
|
closeFunctionEditor: () => closeFunctionEditor(),
|
|
318
329
|
getCanvasMode,
|
|
319
|
-
navigateBack: () => navigateBack(),
|
|
320
|
-
navigateToLevel: (i: number) => navigateToLevel(i),
|
|
321
330
|
openProject: () => openProject(),
|
|
322
331
|
openRecentProject: (root: string) => openRecentProject(root),
|
|
323
|
-
parseMediaEntries,
|
|
324
332
|
renderCanvas: () => renderCanvas(),
|
|
325
333
|
safeRenderRightPanel: () => safeRenderRightPanel(),
|
|
326
334
|
saveFile: () => saveFile(),
|
|
@@ -332,6 +340,15 @@ initQuickSearch();
|
|
|
332
340
|
|
|
333
341
|
tabStrip.mount(document.querySelector("#tab-strip") as HTMLElement);
|
|
334
342
|
|
|
343
|
+
tabBar.mount(document.querySelector("#tab-bar") as HTMLElement, {
|
|
344
|
+
closeFunctionEditor: () => closeFunctionEditor(),
|
|
345
|
+
exportFile,
|
|
346
|
+
getCanvasMode,
|
|
347
|
+
navigateBack: () => navigateBack(),
|
|
348
|
+
navigateToLevel: (i: number) => navigateToLevel(i),
|
|
349
|
+
parseMediaEntries,
|
|
350
|
+
});
|
|
351
|
+
|
|
335
352
|
overlaysPanel.mount({
|
|
336
353
|
getCanvasMode,
|
|
337
354
|
isEditing,
|
|
@@ -366,9 +383,16 @@ initPanelEvents({
|
|
|
366
383
|
initCanvasLiveRender({
|
|
367
384
|
getCanvasMode,
|
|
368
385
|
});
|
|
386
|
+
initCanvasPatcher({
|
|
387
|
+
applyCanvasMediaOverrides,
|
|
388
|
+
enterComponentInlineEdit,
|
|
389
|
+
getCanvasMode,
|
|
390
|
+
registerSubtreeDnD,
|
|
391
|
+
renderOverlays,
|
|
392
|
+
scheduleCanvasRender,
|
|
393
|
+
updateForcedPseudoPreview,
|
|
394
|
+
});
|
|
369
395
|
initCanvasRender({
|
|
370
|
-
closeFunctionEditor: () => closeFunctionEditor(),
|
|
371
|
-
exportFile,
|
|
372
396
|
getCanvasMode,
|
|
373
397
|
get gitDiffState() {
|
|
374
398
|
return gitDiffState;
|
|
@@ -385,20 +409,32 @@ initWelcome({
|
|
|
385
409
|
openNewProject: async () => {
|
|
386
410
|
const result = await openNewProjectModal();
|
|
387
411
|
if (result) {
|
|
388
|
-
openRecentProject(result.root);
|
|
412
|
+
void openRecentProject(result.root);
|
|
389
413
|
}
|
|
390
414
|
},
|
|
391
415
|
openProject: () => openProject(),
|
|
392
416
|
openRecentProject: (root: string) => openRecentProject(root),
|
|
393
417
|
});
|
|
394
418
|
|
|
395
|
-
// Effect-driven canvas rendering
|
|
396
|
-
//
|
|
397
|
-
|
|
419
|
+
// Effect-driven canvas rendering, split into two triggers so document changes can be
|
|
420
|
+
// Distinguished from mode/UI changes:
|
|
421
|
+
// - doc-effect: tracks only the document root reference. Document mutations that were
|
|
422
|
+
// Consumed surgically by the canvas patcher skip the full render here.
|
|
423
|
+
// - ui-effect: tracks canvas mode and UI flags; always schedules a full render.
|
|
424
|
+
// Scheduling is deduped inside scheduleCanvasRender (double-RAF).
|
|
425
|
+
effect(() => {
|
|
426
|
+
const tab = activeTab.value;
|
|
427
|
+
if (tab) {
|
|
428
|
+
const doc = tab.doc.document;
|
|
429
|
+
if (doc && consumePatchedDocument(doc)) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
scheduleCanvasRender();
|
|
434
|
+
});
|
|
398
435
|
effect(() => {
|
|
399
436
|
const tab = activeTab.value;
|
|
400
437
|
if (tab) {
|
|
401
|
-
void tab.doc.document;
|
|
402
438
|
void tab.doc.mode;
|
|
403
439
|
void tab.session.ui.canvasMode;
|
|
404
440
|
void tab.session.ui.editingFunction;
|
|
@@ -408,18 +444,7 @@ effect(() => {
|
|
|
408
444
|
void tab.session.ui.stylebookFilter;
|
|
409
445
|
void tab.session.ui.stylebookCustomizedOnly;
|
|
410
446
|
}
|
|
411
|
-
|
|
412
|
-
_canvasRafId = requestAnimationFrame(() => {
|
|
413
|
-
_canvasRafId = requestAnimationFrame(() => {
|
|
414
|
-
_canvasRafId = 0;
|
|
415
|
-
try {
|
|
416
|
-
renderCanvas();
|
|
417
|
-
} catch (error) {
|
|
418
|
-
console.error("renderCanvas error:", error);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
447
|
+
scheduleCanvasRender();
|
|
423
448
|
});
|
|
424
449
|
|
|
425
450
|
rightPanelMod.mount({
|
|
@@ -483,6 +508,13 @@ function safeRenderRightPanel() {
|
|
|
483
508
|
// Now that renderers are registered, bootstrap
|
|
484
509
|
registerFunctionCompletions();
|
|
485
510
|
|
|
511
|
+
let fsUnsub: (() => void) | null = null;
|
|
512
|
+
/** (Re)subscribe the sidebar to backend filesystem events for the active project. */
|
|
513
|
+
function ensureFsSync() {
|
|
514
|
+
fsUnsub?.();
|
|
515
|
+
fsUnsub = startFsSync({ onContentChange: reloadCleanTab, renderLeftPanel });
|
|
516
|
+
}
|
|
517
|
+
|
|
486
518
|
const _urlParams = new URLSearchParams(location.search);
|
|
487
519
|
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
488
520
|
|
|
@@ -499,7 +531,7 @@ if (_projectParam) {
|
|
|
499
531
|
render();
|
|
500
532
|
const platform = getPlatform();
|
|
501
533
|
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: project probing must not block the initial render
|
|
502
|
-
(async () => {
|
|
534
|
+
void (async () => {
|
|
503
535
|
try {
|
|
504
536
|
const siteCtx = platform.resolveSiteContext
|
|
505
537
|
? await platform.resolveSiteContext(_projectParam)
|
|
@@ -592,14 +624,14 @@ if (_projectParam) {
|
|
|
592
624
|
({ frontmatter } = result);
|
|
593
625
|
parsedMode = result.mode;
|
|
594
626
|
} else {
|
|
595
|
-
parsedDoc = JSON.parse(content);
|
|
627
|
+
parsedDoc = JSON.parse(content) as JxMutableNode;
|
|
596
628
|
}
|
|
597
629
|
|
|
598
630
|
// Open in a tab
|
|
599
631
|
openTab({
|
|
600
632
|
id: fileRelPath,
|
|
601
633
|
documentPath: fileRelPath,
|
|
602
|
-
document: parsedDoc,
|
|
634
|
+
document: parsedDoc as JxMutableNode,
|
|
603
635
|
...(frontmatter != null && { frontmatter }),
|
|
604
636
|
sourceFormat: fileFormat?.name ?? null,
|
|
605
637
|
});
|
|
@@ -621,8 +653,9 @@ if (_projectParam) {
|
|
|
621
653
|
}
|
|
622
654
|
} else {
|
|
623
655
|
// Normal mode: probe for project at server root
|
|
624
|
-
loadProject();
|
|
656
|
+
void loadProject();
|
|
625
657
|
render();
|
|
658
|
+
ensureFsSync();
|
|
626
659
|
}
|
|
627
660
|
|
|
628
661
|
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
@@ -634,18 +667,37 @@ function renderLeftPanel() {
|
|
|
634
667
|
function loadProject() {
|
|
635
668
|
return _loadProject();
|
|
636
669
|
}
|
|
637
|
-
function openProject() {
|
|
638
|
-
|
|
670
|
+
async function openProject() {
|
|
671
|
+
const result = await _openProject({
|
|
639
672
|
renderActivityBar: () => renderActivityBar(),
|
|
640
673
|
renderLeftPanel,
|
|
641
674
|
});
|
|
675
|
+
ensureFsSync();
|
|
676
|
+
return result;
|
|
642
677
|
}
|
|
643
678
|
async function openRecentProject(root: string) {
|
|
644
679
|
try {
|
|
645
680
|
const platform = getPlatform();
|
|
681
|
+
|
|
682
|
+
// Multi-window (desktop): if this window already holds a project, open the chosen one in a new
|
|
683
|
+
// Window (focusing an existing window if it's already open) rather than replacing this project.
|
|
684
|
+
if (projectState && platform.openProjectInNewWindow) {
|
|
685
|
+
await platform.openProjectInNewWindow(root);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Multi-window (desktop): bind THIS window's backend to the project before reading from it. If
|
|
690
|
+
// The project is already open in another window, that window is focused and we bail here.
|
|
691
|
+
if (platform.setWindowProject) {
|
|
692
|
+
const res = await platform.setWindowProject(root);
|
|
693
|
+
if (res.deduped) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
646
698
|
platform.projectRoot = root;
|
|
647
699
|
const content = await platform.readFile("project.json");
|
|
648
|
-
const config = JSON.parse(content);
|
|
700
|
+
const config = JSON.parse(content) as ProjectConfig;
|
|
649
701
|
|
|
650
702
|
closeAllTabs();
|
|
651
703
|
|
|
@@ -654,7 +706,7 @@ async function openRecentProject(root: string) {
|
|
|
654
706
|
dirs: new Map(),
|
|
655
707
|
expanded: new Set(),
|
|
656
708
|
isSiteProject: true,
|
|
657
|
-
name: config.name || root.split("/").pop()
|
|
709
|
+
name: config.name || root.split("/").pop()!,
|
|
658
710
|
projectConfig: config,
|
|
659
711
|
projectRoot: root,
|
|
660
712
|
searchQuery: "",
|
|
@@ -688,6 +740,7 @@ async function openRecentProject(root: string) {
|
|
|
688
740
|
statusMessage(`Opened project: ${requireProjectState().name}`);
|
|
689
741
|
|
|
690
742
|
await openHomePage();
|
|
743
|
+
ensureFsSync();
|
|
691
744
|
} catch (error) {
|
|
692
745
|
statusMessage(`Error: ${errorMessage(error)}`);
|
|
693
746
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch ops — typed descriptors of document mutations, recorded by the mutators in transact.ts
|
|
3
|
+
* during a transaction. The canvas patcher consumes them to update the live canvas DOM surgically
|
|
4
|
+
* instead of re-rendering everything. Ops carry paths, not values: consumers read the post-mutation
|
|
5
|
+
* document as the single source of truth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JxPath } from "../state";
|
|
9
|
+
import type { Tab } from "./tab.js";
|
|
10
|
+
|
|
11
|
+
export type JxPatchOp =
|
|
12
|
+
/** The node's style changed (any depth: base prop, @media block, nested selector). */
|
|
13
|
+
| { op: "set-style"; path: JxPath }
|
|
14
|
+
/** The node's textContent changed. */
|
|
15
|
+
| { op: "set-text"; path: JxPath }
|
|
16
|
+
/** A non-style, non-text node property changed (key = property name). */
|
|
17
|
+
| { op: "set-prop"; path: JxPath; key: string; isEvent?: boolean }
|
|
18
|
+
/** The node's attributes[attr] changed. */
|
|
19
|
+
| { op: "set-attr"; path: JxPath; attr: string }
|
|
20
|
+
/** A node was inserted at parentPath.children[index]. */
|
|
21
|
+
| { op: "insert"; parentPath: JxPath; index: number }
|
|
22
|
+
/** The node at path was removed. */
|
|
23
|
+
| { op: "remove"; path: JxPath }
|
|
24
|
+
/** The node moved between/within children arrays (indices are post-mutation document truth). */
|
|
25
|
+
| { op: "move"; fromPath: JxPath; toParentPath: JxPath; toIndex: number }
|
|
26
|
+
/** The node at path was structurally replaced (wrap, tagName change, $props edit). */
|
|
27
|
+
| { op: "replace"; path: JxPath }
|
|
28
|
+
/** A document-level key changed (state/$media/$head/$elements/imports/$layout) — escalates. */
|
|
29
|
+
| { op: "doc-meta"; key: string };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Value-carrying document mutation, replayable in either direction. Mutators record a
|
|
33
|
+
* forward/inverse pair per change; history applies them for surgical undo/redo and for
|
|
34
|
+
* materializing states from checkpoints — without whole-document snapshots per edit.
|
|
35
|
+
*/
|
|
36
|
+
export type JxDocOp =
|
|
37
|
+
/** Set (or delete, when value is undefined) a key on the node at path. */
|
|
38
|
+
| { op: "set-key"; path: JxPath; key: string; value?: unknown }
|
|
39
|
+
/** Insert a node at parentPath.children[index]. */
|
|
40
|
+
| { op: "insert-child"; parentPath: JxPath; index: number; node: unknown }
|
|
41
|
+
/** Remove parentPath.children[index]. */
|
|
42
|
+
| { op: "remove-child"; parentPath: JxPath; index: number }
|
|
43
|
+
/** Replace parentPath.children[index] with node. */
|
|
44
|
+
| { op: "set-child"; parentPath: JxPath; index: number; node: unknown }
|
|
45
|
+
/** Raw two-splice move: remove fromParent.children[fromIndex], insert at toParent[toIndex]. */
|
|
46
|
+
| {
|
|
47
|
+
op: "move-child";
|
|
48
|
+
fromParentPath: JxPath;
|
|
49
|
+
fromIndex: number;
|
|
50
|
+
toParentPath: JxPath;
|
|
51
|
+
toIndex: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface JxDocOpPair {
|
|
55
|
+
forward: JxDocOp;
|
|
56
|
+
inverse: JxDocOp;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Everything recorded during one transaction. */
|
|
60
|
+
export interface TransactionRecord {
|
|
61
|
+
/** Canvas patch ops (path-only) for surgical DOM updates. */
|
|
62
|
+
ops: JxPatchOp[];
|
|
63
|
+
/** Replayable forward/inverse document ops for patch-based history. */
|
|
64
|
+
docOps: JxDocOpPair[];
|
|
65
|
+
/**
|
|
66
|
+
* False when some mutation in the transaction could not produce a guaranteed-correct inverse
|
|
67
|
+
* (e.g. a move whose parents' paths interact) — history falls back to a checkpoint snapshot.
|
|
68
|
+
*/
|
|
69
|
+
invertible: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The canvas-side consumer of patch batches. Registered by the canvas patcher at studio init;
|
|
74
|
+
* absent in headless tests, where every transaction falls back to the full-render path.
|
|
75
|
+
*/
|
|
76
|
+
export interface PatchConsumer {
|
|
77
|
+
/** Decide whether this batch can be applied surgically in the current canvas state. */
|
|
78
|
+
classify: (tab: Tab, ops: JxPatchOp[]) => { patchable: boolean; reason: string };
|
|
79
|
+
/** Mark a document root reference as surgically consumed (checked by the canvas doc-effect). */
|
|
80
|
+
markConsumed: (docRef: object) => void;
|
|
81
|
+
/** Apply the batch to all ready canvas panels. Throws on any failure (caller escalates). */
|
|
82
|
+
apply: (tab: Tab, ops: JxPatchOp[]) => void;
|
|
83
|
+
/** Schedule a full canvas render as the fallback path, recording the reason. */
|
|
84
|
+
escalate: (reason: string) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let _consumer: PatchConsumer | null = null;
|
|
88
|
+
|
|
89
|
+
export function setPatchConsumer(consumer: PatchConsumer | null) {
|
|
90
|
+
_consumer = consumer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getPatchConsumer(): PatchConsumer | null {
|
|
94
|
+
return _consumer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Recording ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
let _recording: JxPatchOp[] | null = null;
|
|
100
|
+
let _docOps: JxDocOpPair[] | null = null;
|
|
101
|
+
let _invertible = true;
|
|
102
|
+
|
|
103
|
+
/** Begin recording patch ops for a transaction. */
|
|
104
|
+
export function beginRecording() {
|
|
105
|
+
_recording = [];
|
|
106
|
+
_docOps = [];
|
|
107
|
+
_invertible = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Record a patch op describing an in-place mutation. No-op outside a transaction. */
|
|
111
|
+
export function recordPatch(op: JxPatchOp) {
|
|
112
|
+
if (_recording) {
|
|
113
|
+
_recording.push(op);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Record a replayable forward/inverse document-op pair. No-op outside a transaction. */
|
|
118
|
+
export function recordDocOp(pair: JxDocOpPair) {
|
|
119
|
+
if (_docOps) {
|
|
120
|
+
_docOps.push(pair);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Mark the current transaction as non-invertible — history stores a checkpoint instead. */
|
|
125
|
+
export function markNonInvertible() {
|
|
126
|
+
_invertible = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* End recording and return everything recorded. An empty ops array means the mutation ran without
|
|
131
|
+
* instrumentation (unknown change shape) — callers must treat that as not patchable.
|
|
132
|
+
*/
|
|
133
|
+
export function endRecording(): TransactionRecord {
|
|
134
|
+
const record: TransactionRecord = {
|
|
135
|
+
docOps: _docOps ?? [],
|
|
136
|
+
invertible: _invertible,
|
|
137
|
+
ops: _recording ?? [],
|
|
138
|
+
};
|
|
139
|
+
_recording = null;
|
|
140
|
+
_docOps = null;
|
|
141
|
+
_invertible = true;
|
|
142
|
+
return record;
|
|
143
|
+
}
|
package/src/tabs/tab.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
import { effectScope, reactive } from "../reactivity";
|
|
3
3
|
import { formatByName, formatForPath } from "../format/format-host";
|
|
4
|
+
import { normalizeArrayChildren } from "../state";
|
|
4
5
|
import type {
|
|
5
6
|
DocumentStackEntry,
|
|
6
7
|
FunctionEditDef,
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
InlineEditDef,
|
|
11
12
|
} from "../types";
|
|
12
13
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
14
|
+
import type { JxDocOp } from "./patch-ops";
|
|
13
15
|
|
|
14
16
|
export interface TabUi {
|
|
15
17
|
rightTab: string;
|
|
@@ -39,8 +41,16 @@ export interface TabUi {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface HistorySnapshot {
|
|
42
|
-
document
|
|
44
|
+
/** Full document at this state (checkpoint), or null when the ops describe the transition. */
|
|
45
|
+
document: Record<string, unknown> | null;
|
|
46
|
+
/** Selection after this state's transaction. */
|
|
43
47
|
selection: (string | number)[] | null;
|
|
48
|
+
/** Selection before this state's transaction (restored on surgical undo). */
|
|
49
|
+
selectionBefore?: (string | number)[] | null;
|
|
50
|
+
/** Replayable ops transforming the previous state into this one. */
|
|
51
|
+
forwardOps?: JxDocOp[] | null;
|
|
52
|
+
/** Replayable ops transforming this state back into the previous one. */
|
|
53
|
+
inverseOps?: JxDocOp[] | null;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export interface Tab {
|
|
@@ -145,6 +155,10 @@ export function createTab({
|
|
|
145
155
|
}) {
|
|
146
156
|
const scope = effectScope();
|
|
147
157
|
|
|
158
|
+
// Normalize legacy whole-children repeaters to the canonical array-member form before the doc
|
|
159
|
+
// (and its first history checkpoint) are stored.
|
|
160
|
+
normalizeArrayChildren(document);
|
|
161
|
+
|
|
148
162
|
const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
|
|
149
163
|
|
|
150
164
|
const tab = scope.run(() => ({
|