@jxsuite/studio 1.0.0 → 1.2.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 +760 -65
- package/dist/iframe-entry.js.map +16 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +71573 -50074
- package/dist/studio.js.map +123 -88
- package/package.json +11 -9
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +37 -1
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +193 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +56 -3
- package/src/canvas/iframe-render.ts +53 -14
- package/src/canvas/site-style-css.ts +121 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +53 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +9 -387
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +115 -23
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/style-panel.ts +76 -22
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +214 -10
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/css-vars-editor.ts +122 -4
- package/src/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +90 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +46 -1
- package/src/tabs/transact.ts +60 -13
- package/src/types.ts +12 -0
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/utils/canvas-media.ts +23 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
package/src/studio.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { errorMessage } from "@jxsuite/schema/parse";
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
canvasWrap,
|
|
14
|
+
chatPanelEl,
|
|
14
15
|
getNodeAtPath,
|
|
15
16
|
initShellRefs,
|
|
16
17
|
projectState,
|
|
@@ -23,14 +24,20 @@ import {
|
|
|
23
24
|
updateUi,
|
|
24
25
|
} from "./store";
|
|
25
26
|
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
activeTab,
|
|
29
|
+
closeAllTabs,
|
|
30
|
+
openTab,
|
|
31
|
+
setWorkspaceProject,
|
|
32
|
+
workspace,
|
|
33
|
+
} from "./workspace/workspace";
|
|
27
34
|
import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "./tabs/transact";
|
|
28
35
|
import { effect } from "./reactivity";
|
|
29
36
|
|
|
30
37
|
import { view } from "./view";
|
|
31
38
|
|
|
32
39
|
import { isEditing } from "./editor/inline-edit";
|
|
33
|
-
import { applyTransform, initCanvasUtils
|
|
40
|
+
import { applyTransform, initCanvasUtils } from "./canvas/canvas-utils";
|
|
34
41
|
import {
|
|
35
42
|
initCanvasRender,
|
|
36
43
|
renderCanvas,
|
|
@@ -41,6 +48,7 @@ import { consumePatchedDocument, initCanvasPatcher } from "./canvas/canvas-patch
|
|
|
41
48
|
import {
|
|
42
49
|
commitActiveEditSession,
|
|
43
50
|
getEditSnapshot,
|
|
51
|
+
postColorSchemeToLiveHosts,
|
|
44
52
|
setCanvasContextMenuHandler,
|
|
45
53
|
setCanvasSlashHandler,
|
|
46
54
|
setIframePatchEscalation,
|
|
@@ -113,12 +121,15 @@ import { components as _swc } from "./ui/spectrum";
|
|
|
113
121
|
import "./ui/panel-resize.js";
|
|
114
122
|
// Built-in schema-form controls (schema-builder, secret) register on import
|
|
115
123
|
import "./ui/form-controls.js";
|
|
116
|
-
import { initLayers } from "./ui/layers";
|
|
124
|
+
import { initLayers, showSaveDiscardDialog } from "./ui/layers";
|
|
117
125
|
import { initShortcuts } from "./editor/shortcuts";
|
|
118
126
|
import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar";
|
|
119
127
|
import * as toolbarPanel from "./panels/toolbar";
|
|
120
128
|
import * as overlaysPanel from "./panels/overlays";
|
|
129
|
+
import * as frontmatterPanelMod from "./panels/frontmatter-panel";
|
|
121
130
|
import * as rightPanelMod from "./panels/right-panel";
|
|
131
|
+
import * as chatPanelMod from "./panels/chat-panel";
|
|
132
|
+
import { setProjectAdopter } from "./services/project-adoption";
|
|
122
133
|
import * as leftPanelMod from "./panels/left-panel";
|
|
123
134
|
import * as tabStrip from "./panels/tab-strip";
|
|
124
135
|
import * as tabBar from "./panels/tab-bar";
|
|
@@ -127,6 +138,7 @@ import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from ".
|
|
|
127
138
|
import { registerCanvasDndBridge } from "./panels/canvas-dnd-bridge";
|
|
128
139
|
import { defaultDef } from "./panels/shared";
|
|
129
140
|
import { registerFunctionCompletions } from "./panels/editors";
|
|
141
|
+
import { closeFormulaWorkspace } from "./panels/formula-workspace";
|
|
130
142
|
import {
|
|
131
143
|
initBlockActionBar,
|
|
132
144
|
isEditChromeTarget,
|
|
@@ -142,6 +154,7 @@ import { initWelcome } from "./panels/welcome-screen";
|
|
|
142
154
|
import { openAddRepoModal } from "./new-project/add-repo-modal";
|
|
143
155
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
144
156
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
157
|
+
import type { Tab } from "./tabs/tab";
|
|
145
158
|
import type { JxPath } from "./state";
|
|
146
159
|
import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
147
160
|
|
|
@@ -223,19 +236,41 @@ async function navigateToComponent(componentPath: string) {
|
|
|
223
236
|
}
|
|
224
237
|
}
|
|
225
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Leaving a drilled-in component discards its edits (the pop restores the parent). Because saving
|
|
241
|
+
* is explicit, prompt when the child is dirty: Save writes it, Discard drops it, Cancel stays.
|
|
242
|
+
* Returns false to abort navigation.
|
|
243
|
+
*
|
|
244
|
+
* @param {Tab} tab
|
|
245
|
+
*/
|
|
246
|
+
async function confirmLeaveDirtyChild(tab: Tab): Promise<boolean> {
|
|
247
|
+
if (!tab.doc.dirty || !tab.documentPath) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
const name = tab.documentPath.split("/").pop() || "component";
|
|
251
|
+
const choice = await showSaveDiscardDialog("Unsaved Changes", `"${name}" has unsaved changes.`);
|
|
252
|
+
if (choice === "cancel") {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (choice === "save") {
|
|
256
|
+
try {
|
|
257
|
+
await getPlatform().writeFile(tab.documentPath, await serializeDocument(tab));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
statusMessage(`Save error: ${(error as Error).message}`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// "discard": leave without writing — the child's edits are dropped with the popped frame.
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
async function navigateBack() {
|
|
227
268
|
const tab = activeTab.value;
|
|
228
269
|
if (!tab?.session.documentStack || tab.session.documentStack.length === 0) {
|
|
229
270
|
return;
|
|
230
271
|
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
const platform = getPlatform();
|
|
234
|
-
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
235
|
-
} catch (error) {
|
|
236
|
-
const err = error as Error;
|
|
237
|
-
statusMessage(`Save error: ${err.message}`);
|
|
238
|
-
}
|
|
272
|
+
if (!(await confirmLeaveDirtyChild(tab))) {
|
|
273
|
+
return;
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
// Pop the stack
|
|
@@ -262,14 +297,8 @@ async function navigateToLevel(targetIndex: number) {
|
|
|
262
297
|
if (!stack || targetIndex < 0 || targetIndex >= stack.length) {
|
|
263
298
|
return;
|
|
264
299
|
}
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
const platform = getPlatform();
|
|
268
|
-
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
269
|
-
} catch (error) {
|
|
270
|
-
const err = error as Error;
|
|
271
|
-
statusMessage(`Save error: ${err.message}`);
|
|
272
|
-
}
|
|
300
|
+
if (!(await confirmLeaveDirtyChild(tab))) {
|
|
301
|
+
return;
|
|
273
302
|
}
|
|
274
303
|
|
|
275
304
|
const frame = stack[targetIndex] as DocumentStackEntry;
|
|
@@ -393,6 +422,7 @@ initQuickSearch({ openRecentProject: (root: string) => openRecentProject(root) }
|
|
|
393
422
|
tabStrip.mount(document.querySelector("#tab-strip") as HTMLElement);
|
|
394
423
|
|
|
395
424
|
tabBar.mount(document.querySelector("#tab-bar") as HTMLElement, {
|
|
425
|
+
closeFormulaWorkspace: () => closeFormulaWorkspace(),
|
|
396
426
|
closeFunctionEditor: () => closeFunctionEditor(),
|
|
397
427
|
exportFile,
|
|
398
428
|
getCanvasMode,
|
|
@@ -442,6 +472,26 @@ document.addEventListener(
|
|
|
442
472
|
true,
|
|
443
473
|
);
|
|
444
474
|
|
|
475
|
+
// Unsaved-changes guard: saving is explicit (no idle autosave), so warn before the window unloads
|
|
476
|
+
// While any open tab has unsaved edits. For collab tabs, `dirty` reflects the room-level unsaved
|
|
477
|
+
// State; closing loses in-memory edits that were never flushed to disk.
|
|
478
|
+
export function hasUnsavedTabs(): boolean {
|
|
479
|
+
for (const tab of workspace.tabs.values()) {
|
|
480
|
+
if (tab.doc.dirty) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
window.addEventListener("beforeunload", (e: BeforeUnloadEvent) => {
|
|
488
|
+
if (hasUnsavedTabs()) {
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
// Legacy browsers require a truthy returnValue to trigger the native confirm prompt.
|
|
491
|
+
e.returnValue = "";
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
445
495
|
initCanvasUtils({
|
|
446
496
|
getCanvasMode,
|
|
447
497
|
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
@@ -518,10 +568,12 @@ effect(() => {
|
|
|
518
568
|
if (tab) {
|
|
519
569
|
void tab.doc.mode;
|
|
520
570
|
void tab.session.ui.canvasMode;
|
|
571
|
+
void tab.session.ui.editingFormula;
|
|
521
572
|
void tab.session.ui.editingFunction;
|
|
522
573
|
void tab.session.ui.featureToggles;
|
|
523
574
|
void tab.session.ui.preview;
|
|
524
575
|
void tab.session.ui.previewParams;
|
|
576
|
+
void tab.session.ui.previewProps;
|
|
525
577
|
void tab.session.ui.settingsTab;
|
|
526
578
|
void tab.session.ui.showLayout;
|
|
527
579
|
void tab.session.ui.stylebookTab;
|
|
@@ -530,6 +582,12 @@ effect(() => {
|
|
|
530
582
|
}
|
|
531
583
|
scheduleCanvasRender();
|
|
532
584
|
});
|
|
585
|
+
// Color-scheme preview is a document-level attribute flip inside the iframe — deliberately its
|
|
586
|
+
// Own effect, never part of the ui-effect above: flipping the scheme must not re-render.
|
|
587
|
+
effect(() => {
|
|
588
|
+
const s = activeTab.value?.session.ui.previewColorScheme;
|
|
589
|
+
postColorSchemeToLiveHosts(s === "light" || s === "dark" ? s : null);
|
|
590
|
+
});
|
|
533
591
|
|
|
534
592
|
rightPanelMod.mount({
|
|
535
593
|
getCanvasMode,
|
|
@@ -537,6 +595,15 @@ rightPanelMod.mount({
|
|
|
537
595
|
renderCanvas: () => renderCanvas(),
|
|
538
596
|
});
|
|
539
597
|
|
|
598
|
+
// Above-canvas frontmatter Properties panel (content-collection docs, edit mode).
|
|
599
|
+
frontmatterPanelMod.mount({ getCanvasMode });
|
|
600
|
+
|
|
601
|
+
// The persistent AI chat sidebar — mounts once, available with or without a project/document.
|
|
602
|
+
chatPanelMod.mount(chatPanelEl);
|
|
603
|
+
// The assistant's create_project tool adopts freshly scaffolded projects through the same
|
|
604
|
+
// Flow as the recent-projects list.
|
|
605
|
+
setProjectAdopter(openRecentProject);
|
|
606
|
+
|
|
540
607
|
leftPanelMod.mount({
|
|
541
608
|
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
542
609
|
defBadgeLabel,
|
|
@@ -568,6 +635,8 @@ leftPanelMod.mount({
|
|
|
568
635
|
registerRenderer("leftPanel", () => leftPanelMod.render());
|
|
569
636
|
registerRenderer("canvas", () => renderCanvas());
|
|
570
637
|
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
638
|
+
registerRenderer("frontmatterPanel", () => frontmatterPanelMod.render());
|
|
639
|
+
registerRenderer("chatPanel", () => chatPanelMod.render());
|
|
571
640
|
registerRenderer("overlays", () => overlaysPanel.render());
|
|
572
641
|
setStatusbarRenderer(() => renderStatusbar());
|
|
573
642
|
mountStatusbar();
|
|
@@ -667,6 +736,7 @@ if (_projectParam) {
|
|
|
667
736
|
searchQuery: "",
|
|
668
737
|
selectedPath: siteCtx.fileRelPath || null,
|
|
669
738
|
});
|
|
739
|
+
setWorkspaceProject(siteCtx.sitePath, siteCtx.projectConfig || null);
|
|
670
740
|
|
|
671
741
|
refreshExtensionUi(platform);
|
|
672
742
|
await autoSyncProjectOnOpen();
|
|
@@ -859,6 +929,7 @@ async function openRecentProject(root: string) {
|
|
|
859
929
|
searchQuery: "",
|
|
860
930
|
selectedPath: null,
|
|
861
931
|
});
|
|
932
|
+
setWorkspaceProject(root, config);
|
|
862
933
|
|
|
863
934
|
await autoSyncProjectOnOpen();
|
|
864
935
|
await ensureDependenciesInstalled();
|
|
@@ -918,7 +989,6 @@ initShortcuts(() => ({
|
|
|
918
989
|
openProject,
|
|
919
990
|
panX: view.panX,
|
|
920
991
|
panY: view.panY,
|
|
921
|
-
positionZoomIndicator,
|
|
922
992
|
saveFile,
|
|
923
993
|
setPan: (x, y) => {
|
|
924
994
|
view.panX = x;
|
|
@@ -926,35 +996,3 @@ initShortcuts(() => ({
|
|
|
926
996
|
view.needsCenter = false;
|
|
927
997
|
},
|
|
928
998
|
}));
|
|
929
|
-
|
|
930
|
-
// ─── Autosave (registered as update middleware) ──────────────────────────────
|
|
931
|
-
|
|
932
|
-
const AUTO_SAVE_DELAY = 2000;
|
|
933
|
-
|
|
934
|
-
function scheduleAutosave() {
|
|
935
|
-
const tab = activeTab.value;
|
|
936
|
-
if (!tab?.fileHandle || !tab.doc.dirty) {
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
if (view.autosaveTimer) {
|
|
940
|
-
clearTimeout(view.autosaveTimer);
|
|
941
|
-
}
|
|
942
|
-
view.autosaveTimer = setTimeout(async () => {
|
|
943
|
-
const t = activeTab.value;
|
|
944
|
-
if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
|
|
945
|
-
try {
|
|
946
|
-
const writable = await t.fileHandle.createWritable();
|
|
947
|
-
await writable.write(await serializeDocument(t));
|
|
948
|
-
await writable.close();
|
|
949
|
-
t.doc.dirty = false;
|
|
950
|
-
statusMessage("Auto-saved");
|
|
951
|
-
} catch {}
|
|
952
|
-
}
|
|
953
|
-
}, AUTO_SAVE_DELAY);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
effect(() => {
|
|
957
|
-
if (activeTab.value?.doc.dirty) {
|
|
958
|
-
scheduleAutosave();
|
|
959
|
-
}
|
|
960
|
-
});
|
package/src/tabs/patch-ops.ts
CHANGED
|
@@ -40,12 +40,26 @@ export type JxPatchOp =
|
|
|
40
40
|
export type { JxDocOp } from "@jxsuite/collab/ops";
|
|
41
41
|
export type { JxDocOpPair };
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A frontmatter key change, replayable in either direction. Frontmatter lives outside the document
|
|
45
|
+
* tree (`tab.doc.content.frontmatter`, split off by the format host), so these are studio-local and
|
|
46
|
+
* never enter the shared `JxDocOp` vocabulary the collab bridge and canvas iframe mirror.
|
|
47
|
+
* `undefined` means the key was/becomes absent.
|
|
48
|
+
*/
|
|
49
|
+
export interface JxFmOp {
|
|
50
|
+
field: string;
|
|
51
|
+
before?: unknown;
|
|
52
|
+
after?: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/** Everything recorded during one transaction. */
|
|
44
56
|
export interface TransactionRecord {
|
|
45
57
|
/** Canvas patch ops (path-only) for surgical DOM updates. */
|
|
46
58
|
ops: JxPatchOp[];
|
|
47
59
|
/** Replayable forward/inverse document ops for patch-based history. */
|
|
48
60
|
docOps: JxDocOpPair[];
|
|
61
|
+
/** Replayable frontmatter key changes (content-mode docs) for history undo/redo. */
|
|
62
|
+
fmOps: JxFmOp[];
|
|
49
63
|
/**
|
|
50
64
|
* False when some mutation in the transaction could not produce a guaranteed-correct inverse
|
|
51
65
|
* (e.g. a move whose parents' paths interact) — history falls back to a checkpoint snapshot.
|
|
@@ -86,12 +100,14 @@ export function getPatchConsumer(): PatchConsumer | null {
|
|
|
86
100
|
|
|
87
101
|
let _recording: JxPatchOp[] | null = null;
|
|
88
102
|
let _docOps: JxDocOpPair[] | null = null;
|
|
103
|
+
let _fmOps: JxFmOp[] | null = null;
|
|
89
104
|
let _invertible = true;
|
|
90
105
|
|
|
91
106
|
/** Begin recording patch ops for a transaction. */
|
|
92
107
|
export function beginRecording() {
|
|
93
108
|
_recording = [];
|
|
94
109
|
_docOps = [];
|
|
110
|
+
_fmOps = [];
|
|
95
111
|
_invertible = true;
|
|
96
112
|
}
|
|
97
113
|
|
|
@@ -109,6 +125,13 @@ export function recordDocOp(pair: JxDocOpPair) {
|
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
/** Record a replayable frontmatter key change. No-op outside a transaction. */
|
|
129
|
+
export function recordFmOp(op: JxFmOp) {
|
|
130
|
+
if (_fmOps) {
|
|
131
|
+
_fmOps.push(op);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
/** Mark the current transaction as non-invertible — history stores a checkpoint instead. */
|
|
113
136
|
export function markNonInvertible() {
|
|
114
137
|
_invertible = false;
|
|
@@ -121,11 +144,13 @@ export function markNonInvertible() {
|
|
|
121
144
|
export function endRecording(): TransactionRecord {
|
|
122
145
|
const record: TransactionRecord = {
|
|
123
146
|
docOps: _docOps ?? [],
|
|
147
|
+
fmOps: _fmOps ?? [],
|
|
124
148
|
invertible: _invertible,
|
|
125
149
|
ops: _recording ?? [],
|
|
126
150
|
};
|
|
127
151
|
_recording = null;
|
|
128
152
|
_docOps = null;
|
|
153
|
+
_fmOps = null;
|
|
129
154
|
_invertible = true;
|
|
130
155
|
return record;
|
|
131
156
|
}
|
package/src/tabs/tab.ts
CHANGED
|
@@ -4,14 +4,16 @@ import { formatByName, formatForPath } from "../format/format-host";
|
|
|
4
4
|
import { normalizeArrayChildren } from "../state";
|
|
5
5
|
import type {
|
|
6
6
|
DocumentStackEntry,
|
|
7
|
+
FormulaEditDef,
|
|
7
8
|
FunctionEditDef,
|
|
8
9
|
GitBranchesResult,
|
|
9
10
|
GitDiffState,
|
|
10
11
|
GitStatusResult,
|
|
11
12
|
InlineEditDef,
|
|
13
|
+
JsonValue,
|
|
12
14
|
} from "../types";
|
|
13
15
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
14
|
-
import type { JxDocOp } from "./patch-ops";
|
|
16
|
+
import type { JxDocOp, JxFmOp } from "./patch-ops";
|
|
15
17
|
|
|
16
18
|
export interface TabUi {
|
|
17
19
|
rightTab: string;
|
|
@@ -20,13 +22,33 @@ export interface TabUi {
|
|
|
20
22
|
preview: boolean;
|
|
21
23
|
/** Show elements inherited from the page's layout (pages with an effective layout only). */
|
|
22
24
|
showLayout: boolean;
|
|
25
|
+
/** Above-canvas frontmatter Properties accordion expanded (content-collection docs, edit mode). */
|
|
26
|
+
frontmatterOpen: boolean;
|
|
23
27
|
/** Chosen literal values for dynamic route params (e.g. { sku: "mini-trencher" }). */
|
|
24
28
|
previewParams: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Chosen test values for a component doc's props (state entries), seeded into the canvas render
|
|
31
|
+
* so a non-instantiated component previews with real data (M6) — the previewParams mirror.
|
|
32
|
+
*/
|
|
33
|
+
previewProps: Record<string, JsonValue> | null;
|
|
25
34
|
zoom: number;
|
|
35
|
+
/**
|
|
36
|
+
* Edit-mode content zoom — browser-page-zoom semantics (content reflows at the zoomed effective
|
|
37
|
+
* width while the canvas footprint stays fixed), unlike `zoom` which pan/zooms the whole canvas.
|
|
38
|
+
*/
|
|
39
|
+
editZoom: number;
|
|
26
40
|
activeMedia: string | null;
|
|
27
41
|
activeSelector: string | null;
|
|
28
42
|
editingFunction: FunctionEditDef | null;
|
|
43
|
+
/** Full-screen formula workspace target ($expression editing); editingFunction wins if both set. */
|
|
44
|
+
editingFormula: FormulaEditDef | null;
|
|
29
45
|
featureToggles: Record<string, boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Canvas color-scheme preview: follow the OS ("auto") or force a scheme. Drives the
|
|
48
|
+
* data-color-scheme attribute on the canvas iframe root (spec §9.5) and, in the style sidebar,
|
|
49
|
+
* which scheme layer edits target.
|
|
50
|
+
*/
|
|
51
|
+
previewColorScheme: "auto" | "light" | "dark";
|
|
30
52
|
styleSections: Record<string, boolean>;
|
|
31
53
|
inspectorSections: Record<string, boolean>;
|
|
32
54
|
styleShorthands: Record<string, boolean>;
|
|
@@ -46,6 +68,18 @@ export interface TabUi {
|
|
|
46
68
|
pendingInlineEdit: InlineEditDef | null;
|
|
47
69
|
}
|
|
48
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A live (iframe-evaluated) expression preview retained per editing target (M6). Stored on
|
|
73
|
+
* `session.canvas` beside `scope` — plain wire data (path-key → display-string pairs), rebuilt into
|
|
74
|
+
* an ExpressionPreview by services/live-preview.ts on read.
|
|
75
|
+
*/
|
|
76
|
+
export interface StoredLivePreview {
|
|
77
|
+
/** Serialized expression + context the values were computed for (staleness check). */
|
|
78
|
+
key: string;
|
|
79
|
+
values: [string, string][];
|
|
80
|
+
error: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
interface HistorySnapshot {
|
|
50
84
|
/** Full document at this state (checkpoint), or null when the ops describe the transition. */
|
|
51
85
|
document: Record<string, unknown> | null;
|
|
@@ -57,6 +91,8 @@ interface HistorySnapshot {
|
|
|
57
91
|
forwardOps?: JxDocOp[] | null;
|
|
58
92
|
/** Replayable ops transforming this state back into the previous one. */
|
|
59
93
|
inverseOps?: JxDocOp[] | null;
|
|
94
|
+
/** Frontmatter key changes in this transaction (before/after values, replayed both ways). */
|
|
95
|
+
fmOps?: JxFmOp[] | null;
|
|
60
96
|
}
|
|
61
97
|
|
|
62
98
|
export interface Tab {
|
|
@@ -89,6 +125,9 @@ export interface Tab {
|
|
|
89
125
|
// The bridge as a `dataScope` message and read by the data-explorer panel. Plain data now —
|
|
90
126
|
// The old live `EffectScope` (with `.stop()`) moved into the iframe realm with buildScope.
|
|
91
127
|
scope: Record<string, unknown> | null;
|
|
128
|
+
// Latest live (iframe-evaluated) expression previews keyed by editing-target id, stored
|
|
129
|
+
// Beside `scope` the same way (services/live-preview.ts owns reads/writes).
|
|
130
|
+
livePreviews: Record<string, StoredLivePreview> | null;
|
|
92
131
|
error: string | null;
|
|
93
132
|
pendingInlineEdit: InlineEditDef | null;
|
|
94
133
|
};
|
|
@@ -109,8 +148,11 @@ function createDefaultUi(canvasMode: string, preview = false) {
|
|
|
109
148
|
activeMedia: null,
|
|
110
149
|
activeSelector: null,
|
|
111
150
|
canvasMode,
|
|
151
|
+
editZoom: 1,
|
|
152
|
+
editingFormula: null,
|
|
112
153
|
editingFunction: null,
|
|
113
154
|
featureToggles: {},
|
|
155
|
+
frontmatterOpen: true,
|
|
114
156
|
gitBranches: null,
|
|
115
157
|
gitCommitMessage: "",
|
|
116
158
|
gitDiffState: null,
|
|
@@ -120,7 +162,9 @@ function createDefaultUi(canvasMode: string, preview = false) {
|
|
|
120
162
|
inspectorSections: {},
|
|
121
163
|
pendingInlineEdit: null,
|
|
122
164
|
preview,
|
|
165
|
+
previewColorScheme: "auto" as const,
|
|
123
166
|
previewParams: {},
|
|
167
|
+
previewProps: null,
|
|
124
168
|
rightTab: "properties",
|
|
125
169
|
settingsTab: "stylebook",
|
|
126
170
|
showLayout: true,
|
|
@@ -204,6 +248,7 @@ export function createTab({
|
|
|
204
248
|
session: reactive({
|
|
205
249
|
canvas: {
|
|
206
250
|
error: null,
|
|
251
|
+
livePreviews: null,
|
|
207
252
|
pendingInlineEdit: null,
|
|
208
253
|
scope: null,
|
|
209
254
|
status: "idle",
|
package/src/tabs/transact.ts
CHANGED
|
@@ -8,16 +8,23 @@ import {
|
|
|
8
8
|
endRecording,
|
|
9
9
|
getPatchConsumer,
|
|
10
10
|
recordDocOp,
|
|
11
|
+
recordFmOp,
|
|
11
12
|
recordPatch,
|
|
12
13
|
} from "./patch-ops";
|
|
13
14
|
|
|
14
|
-
import type { JxDocOp, JxDocOpPair, TransactionRecord } from "./patch-ops";
|
|
15
|
+
import type { JxDocOp, JxDocOpPair, JxFmOp, TransactionRecord } from "./patch-ops";
|
|
15
16
|
|
|
16
17
|
import type { Tab } from "../tabs/tab";
|
|
17
18
|
import type { JxPath } from "../state";
|
|
18
19
|
|
|
19
20
|
import type { JsonValue } from "../types";
|
|
20
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
JxAttributeValue,
|
|
23
|
+
JxEventBinding,
|
|
24
|
+
JxMutableNode,
|
|
25
|
+
JxStateObject,
|
|
26
|
+
JxStyle,
|
|
27
|
+
} from "@jxsuite/schema/types";
|
|
21
28
|
import { ensureNestedStyle, getNestedStyle, isEventBinding } from "@jxsuite/schema/guards";
|
|
22
29
|
|
|
23
30
|
const HISTORY_LIMIT = 100;
|
|
@@ -205,10 +212,14 @@ function pushHistoryEntry(
|
|
|
205
212
|
selectionBefore: JxPath | null,
|
|
206
213
|
) {
|
|
207
214
|
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
208
|
-
const useOps =
|
|
215
|
+
const useOps =
|
|
216
|
+
patchHistoryEnabled() &&
|
|
217
|
+
record.invertible &&
|
|
218
|
+
(record.docOps.length > 0 || record.fmOps.length > 0);
|
|
209
219
|
const needCheckpoint = !useOps || truncated.length % CHECKPOINT_INTERVAL === 0;
|
|
210
220
|
truncated.push({
|
|
211
221
|
document: needCheckpoint ? jsonClone(raw) : null,
|
|
222
|
+
fmOps: useOps && record.fmOps.length > 0 ? record.fmOps : null,
|
|
212
223
|
forwardOps: useOps ? record.docOps.map((p) => p.forward) : null,
|
|
213
224
|
inverseOps: useOps ? record.docOps.map((p) => p.inverse) : null,
|
|
214
225
|
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
@@ -471,14 +482,23 @@ export function undo(tab: Tab) {
|
|
|
471
482
|
}
|
|
472
483
|
const entry = tab.history.snapshots[tab.history.index]!;
|
|
473
484
|
const inverseOps = patchHistoryEnabled() ? entry.inverseOps : null;
|
|
474
|
-
|
|
485
|
+
const fmOps = patchHistoryEnabled() ? entry.fmOps : null;
|
|
486
|
+
if ((inverseOps && inverseOps.length > 0) || (fmOps && fmOps.length > 0)) {
|
|
475
487
|
// Surgical path: apply inverse ops through the normal transaction pipeline (the canvas
|
|
476
488
|
// Patches in place when it can), in reverse recording order.
|
|
477
489
|
transactDoc(
|
|
478
490
|
tab,
|
|
479
491
|
(t) => {
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
if (inverseOps) {
|
|
493
|
+
for (let i = inverseOps.length - 1; i >= 0; i--) {
|
|
494
|
+
applyDocOp(t, toRaw(inverseOps[i]) as JxDocOp);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (fmOps) {
|
|
498
|
+
for (let i = fmOps.length - 1; i >= 0; i--) {
|
|
499
|
+
const op = toRaw(fmOps[i]) as JxFmOp;
|
|
500
|
+
applyFmState(t, op.field, op.before);
|
|
501
|
+
}
|
|
482
502
|
}
|
|
483
503
|
},
|
|
484
504
|
{ origin: "history", skipHistory: true },
|
|
@@ -505,12 +525,21 @@ export function redo(tab: Tab) {
|
|
|
505
525
|
}
|
|
506
526
|
const entry = tab.history.snapshots[tab.history.index + 1]!;
|
|
507
527
|
const forwardOps = patchHistoryEnabled() ? entry.forwardOps : null;
|
|
508
|
-
|
|
528
|
+
const fmOps = patchHistoryEnabled() ? entry.fmOps : null;
|
|
529
|
+
if ((forwardOps && forwardOps.length > 0) || (fmOps && fmOps.length > 0)) {
|
|
509
530
|
transactDoc(
|
|
510
531
|
tab,
|
|
511
532
|
(t) => {
|
|
512
|
-
|
|
513
|
-
|
|
533
|
+
if (forwardOps) {
|
|
534
|
+
for (const op of forwardOps) {
|
|
535
|
+
applyDocOp(t, toRaw(op) as JxDocOp);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (fmOps) {
|
|
539
|
+
for (const op of fmOps) {
|
|
540
|
+
const raw = toRaw(op) as JxFmOp;
|
|
541
|
+
applyFmState(t, raw.field, raw.after);
|
|
542
|
+
}
|
|
514
543
|
}
|
|
515
544
|
},
|
|
516
545
|
{ origin: "history", skipHistory: true },
|
|
@@ -746,13 +775,13 @@ export function mutateUpdateStyle(tab: Tab, path: JxPath, prop: string, value: s
|
|
|
746
775
|
* @param {Tab} tab
|
|
747
776
|
* @param {JxPath} path
|
|
748
777
|
* @param {string} attr
|
|
749
|
-
* @param {
|
|
778
|
+
* @param {JxAttributeValue | undefined} value — a literal, `$ref` binding, or `${}` template.
|
|
750
779
|
*/
|
|
751
780
|
export function mutateUpdateAttribute(
|
|
752
781
|
tab: Tab,
|
|
753
782
|
path: JxPath,
|
|
754
783
|
attr: string,
|
|
755
|
-
value?:
|
|
784
|
+
value?: JxAttributeValue | undefined,
|
|
756
785
|
) {
|
|
757
786
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
758
787
|
const attrsBefore = cloneValue(node.attributes);
|
|
@@ -1196,10 +1225,28 @@ export function mutateRenameSwitchCase(tab: Tab, path: JxPath, oldName: string,
|
|
|
1196
1225
|
* @param {JsonValue} value
|
|
1197
1226
|
*/
|
|
1198
1227
|
export function mutateUpdateFrontmatter(tab: Tab, field: string, value?: JsonValue) {
|
|
1199
|
-
|
|
1228
|
+
const fm = tab.doc.content.frontmatter;
|
|
1229
|
+
const before = Object.hasOwn(fm, field) ? cloneValue(fm[field]) : undefined;
|
|
1230
|
+
const deletes = value === undefined || value === null || value === "";
|
|
1231
|
+
if (deletes) {
|
|
1232
|
+
delete fm[field];
|
|
1233
|
+
} else {
|
|
1234
|
+
fm[field] = value;
|
|
1235
|
+
}
|
|
1236
|
+
const after = deletes ? undefined : cloneValue(value);
|
|
1237
|
+
recordFmOp({ after, before, field });
|
|
1238
|
+
tab.doc.dirty = true;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Restore a frontmatter key to a recorded history state: `undefined` means the key was absent.
|
|
1243
|
+
* Unlike `mutateUpdateFrontmatter`, values like `""`/`false`/`null` are written verbatim (source
|
|
1244
|
+
* round-trips can put them there) and no fm op is recorded — history replay must not re-record.
|
|
1245
|
+
*/
|
|
1246
|
+
function applyFmState(tab: Tab, field: string, value: unknown) {
|
|
1247
|
+
if (value === undefined) {
|
|
1200
1248
|
delete tab.doc.content.frontmatter[field];
|
|
1201
1249
|
} else {
|
|
1202
1250
|
tab.doc.content.frontmatter[field] = value;
|
|
1203
1251
|
}
|
|
1204
|
-
tab.doc.dirty = true;
|
|
1205
1252
|
}
|
package/src/types.ts
CHANGED
|
@@ -427,6 +427,18 @@ export interface FunctionEditDef {
|
|
|
427
427
|
parameters?: string[];
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Identifies which document position's `$expression` the full-screen formula workspace is editing:
|
|
432
|
+
* a state entry (`type: "def"` + defName) or an element event binding (`type: "event"` + path +
|
|
433
|
+
* eventKey). Mirrors FunctionEditDef, the Monaco function editor's target shape.
|
|
434
|
+
*/
|
|
435
|
+
export interface FormulaEditDef {
|
|
436
|
+
type: "def" | "event";
|
|
437
|
+
defName?: string;
|
|
438
|
+
path?: JxPath;
|
|
439
|
+
eventKey?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
430
442
|
export interface GitDiffState {
|
|
431
443
|
filePath: string;
|
|
432
444
|
originalContent: string;
|