@jxsuite/studio 0.37.1 → 1.1.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 +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +103709 -81001
- package/dist/studio.js.map +201 -157
- package/package.json +47 -45
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- 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 +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +132 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -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/context-menu.ts +1 -1
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +56 -1
- package/src/format/format-host.ts +63 -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/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +241 -0
- 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 +217 -460
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- 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/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -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/monaco-setup.ts +75 -26
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +12 -1
- package/src/store.ts +4 -0
- package/src/studio.ts +107 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +61 -14
- package/src/types.ts +132 -1
- 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/form-controls.ts +322 -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/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
- package/src/settings/content-types-editor.ts +0 -599
|
@@ -2,41 +2,129 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Settings modal — site-wide project settings (CSS variables, definitions, content types, head,
|
|
4
4
|
* general). Modeled after VS Code / Obsidian settings panels: left sidebar nav + right content
|
|
5
|
-
* area.
|
|
5
|
+
* area. Sections come from a registry: built-ins register at module init, and extensions add
|
|
6
|
+
* descriptor-contributed sections through `registerSettingsSection`.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { html } from "lit-html";
|
|
9
10
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
10
11
|
import { ref } from "lit-html/directives/ref.js";
|
|
11
12
|
import { renderDefsEditor } from "./defs-editor";
|
|
12
|
-
import { renderContentTypesEditor } from "./content-types-editor";
|
|
13
13
|
import { renderCssVarsEditor } from "./css-vars-editor";
|
|
14
14
|
import { renderHeadEditor } from "./head-editor";
|
|
15
15
|
import { renderGeneralSettings } from "./general-settings";
|
|
16
16
|
import { renderDependenciesEditor } from "./dependencies-editor";
|
|
17
17
|
import { openModal } from "../ui/layers";
|
|
18
18
|
|
|
19
|
+
// ─── Section registry ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** A settings-modal section: nav entry plus a renderer for the content area. */
|
|
22
|
+
export interface SettingsSection {
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
/** Nav icon name (reserved for future nav treatments). */
|
|
26
|
+
icon?: string | undefined;
|
|
27
|
+
/** Sort position — lower orders render higher in the nav. */
|
|
28
|
+
order: number;
|
|
29
|
+
render: (container: HTMLElement) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sectionRegistry = new Map<string, SettingsSection>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register (or replace) a settings section. Extensions use this hook to contribute
|
|
36
|
+
* descriptor-driven sections; built-ins register below at module init.
|
|
37
|
+
*
|
|
38
|
+
* @param {SettingsSection} section
|
|
39
|
+
*/
|
|
40
|
+
export function registerSettingsSection(section: SettingsSection): void {
|
|
41
|
+
sectionRegistry.set(section.key, section);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove a registered section — used when a descriptor-contributed section's extension is disabled
|
|
46
|
+
* (see ./extension-sections). Built-ins are never unregistered.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} key
|
|
49
|
+
*/
|
|
50
|
+
export function unregisterSettingsSection(key: string): void {
|
|
51
|
+
sectionRegistry.delete(key);
|
|
52
|
+
if (_activeSection === key) {
|
|
53
|
+
_activeSection = "general";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registered sections sorted by order (registration order breaks ties). */
|
|
58
|
+
function sortedSections(): SettingsSection[] {
|
|
59
|
+
return [...sectionRegistry.values()].toSorted((a, b) => a.order - b.order);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Built-in sections — orders preserve the historical display order
|
|
63
|
+
registerSettingsSection({
|
|
64
|
+
icon: "sp-icon-properties",
|
|
65
|
+
key: "general",
|
|
66
|
+
label: "General",
|
|
67
|
+
order: 10,
|
|
68
|
+
render: renderGeneralSettings,
|
|
69
|
+
});
|
|
70
|
+
registerSettingsSection({
|
|
71
|
+
icon: "sp-icon-file-single-web-page",
|
|
72
|
+
key: "head",
|
|
73
|
+
label: "Head",
|
|
74
|
+
order: 20,
|
|
75
|
+
render: renderHeadEditor,
|
|
76
|
+
});
|
|
77
|
+
registerSettingsSection({
|
|
78
|
+
icon: "sp-icon-brush",
|
|
79
|
+
key: "cssVars",
|
|
80
|
+
label: "CSS Variables",
|
|
81
|
+
order: 30,
|
|
82
|
+
render: renderCssVarsEditor,
|
|
83
|
+
});
|
|
84
|
+
registerSettingsSection({
|
|
85
|
+
icon: "sp-icon-data",
|
|
86
|
+
key: "definitions",
|
|
87
|
+
label: "Definitions",
|
|
88
|
+
order: 40,
|
|
89
|
+
render: renderDefsEditor,
|
|
90
|
+
});
|
|
91
|
+
// Content Types is no longer a built-in: @jxsuite/parser contributes it (order 50) through its
|
|
92
|
+
// Content class descriptor's $studio.settings block, registered via ./extension-sections.
|
|
93
|
+
registerSettingsSection({
|
|
94
|
+
icon: "sp-icon-box",
|
|
95
|
+
key: "dependencies",
|
|
96
|
+
label: "Dependencies",
|
|
97
|
+
order: 60,
|
|
98
|
+
render: renderDependenciesEditor,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── Modal state ──────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
19
103
|
let _handle: ReturnType<typeof openModal> | null = null;
|
|
20
104
|
|
|
21
105
|
let _activeSection = "general";
|
|
22
106
|
|
|
23
107
|
let _contentEl: HTMLElement | null = null;
|
|
24
108
|
|
|
25
|
-
const sections = [
|
|
26
|
-
{ icon: "sp-icon-properties", key: "general", label: "General" },
|
|
27
|
-
{ icon: "sp-icon-file-single-web-page", key: "head", label: "Head" },
|
|
28
|
-
{ icon: "sp-icon-brush", key: "cssVars", label: "CSS Variables" },
|
|
29
|
-
{ icon: "sp-icon-data", key: "definitions", label: "Definitions" },
|
|
30
|
-
{ icon: "sp-icon-view-grid", key: "contentTypes", label: "Content Types" },
|
|
31
|
-
{ icon: "sp-icon-box", key: "dependencies", label: "Dependencies" },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
109
|
export function openSettingsModal() {
|
|
35
110
|
if (_handle) {
|
|
36
111
|
return;
|
|
37
112
|
}
|
|
38
113
|
_activeSection = "general";
|
|
39
114
|
renderModal();
|
|
115
|
+
// Refresh descriptor-contributed sections (cached payloads make this cheap) and rerender the
|
|
116
|
+
// Nav once they land. Lazy import breaks the settings-modal ↔ extension-sections module cycle.
|
|
117
|
+
void import("./extension-sections")
|
|
118
|
+
.then(async ({ syncExtensionSettingsSections }) => {
|
|
119
|
+
await syncExtensionSettingsSections();
|
|
120
|
+
if (_handle) {
|
|
121
|
+
renderModal();
|
|
122
|
+
renderActiveSection();
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch(() => {
|
|
126
|
+
// Contributed sections are optional — the built-ins render regardless.
|
|
127
|
+
});
|
|
40
128
|
}
|
|
41
129
|
|
|
42
130
|
export function closeSettingsModal() {
|
|
@@ -73,7 +161,7 @@ function renderModal() {
|
|
|
73
161
|
</div>
|
|
74
162
|
<div class="settings-modal-body">
|
|
75
163
|
<nav class="settings-modal-nav">
|
|
76
|
-
${
|
|
164
|
+
${sortedSections().map(
|
|
77
165
|
(s) => html`
|
|
78
166
|
<button
|
|
79
167
|
class=${classMap({
|
|
@@ -111,34 +199,5 @@ function renderActiveSection() {
|
|
|
111
199
|
if (!_handle || !_contentEl) {
|
|
112
200
|
return;
|
|
113
201
|
}
|
|
114
|
-
|
|
115
|
-
switch (_activeSection) {
|
|
116
|
-
case "general": {
|
|
117
|
-
renderGeneralSettings(_contentEl);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
case "head": {
|
|
121
|
-
renderHeadEditor(_contentEl);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case "cssVars": {
|
|
125
|
-
renderCssVarsEditor(_contentEl);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case "definitions": {
|
|
129
|
-
renderDefsEditor(_contentEl);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case "contentTypes": {
|
|
133
|
-
renderContentTypesEditor(_contentEl);
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case "dependencies": {
|
|
137
|
-
renderDependenciesEditor(_contentEl);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
default: {
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
202
|
+
sectionRegistry.get(_activeSection)?.render(_contentEl);
|
|
144
203
|
}
|
package/src/site-context.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { projectState, requireProjectState, setProjectState } from "./store";
|
|
10
10
|
import { getPlatform } from "./platform";
|
|
11
|
+
import { setWorkspaceProject, workspace } from "./workspace/workspace";
|
|
11
12
|
|
|
12
13
|
import type {
|
|
13
14
|
JxElement,
|
|
@@ -258,7 +259,9 @@ function fillSlots(
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
/**
|
|
261
|
-
* Update the project's project.json with a partial patch and persist to disk.
|
|
262
|
+
* Update the project's project.json with a partial patch and persist to disk. A patch touching
|
|
263
|
+
* `extensions` invalidates the format/extensions caches and refreshes the editor's per-project
|
|
264
|
+
* schemas plus the contributed settings sections — the enabled-extension surface just changed.
|
|
262
265
|
*
|
|
263
266
|
* @param {Partial<ProjectConfig>} patch - Fields to merge into the current projectConfig
|
|
264
267
|
*/
|
|
@@ -270,4 +273,12 @@ export async function updateSiteConfig(patch: Partial<ProjectConfig>) {
|
|
|
270
273
|
} as ProjectConfig;
|
|
271
274
|
await platform.writeFile("project.json", JSON.stringify(config, null, 2));
|
|
272
275
|
setProjectState({ ...requireProjectState(), projectConfig: config });
|
|
276
|
+
setWorkspaceProject(workspace.projectRoot, config);
|
|
277
|
+
if ("extensions" in patch) {
|
|
278
|
+
const { loadFormats, refreshExtensionUi, refreshFormats } =
|
|
279
|
+
await import("./format/format-host");
|
|
280
|
+
refreshFormats();
|
|
281
|
+
void loadFormats();
|
|
282
|
+
refreshExtensionUi(platform);
|
|
283
|
+
}
|
|
273
284
|
}
|
package/src/store.ts
CHANGED
|
@@ -37,16 +37,20 @@ export let canvasWrap = null as unknown as HTMLElement;
|
|
|
37
37
|
export let activityBar = null as unknown as HTMLElement;
|
|
38
38
|
export let leftPanel = null as unknown as HTMLElement;
|
|
39
39
|
export let rightPanel = null as unknown as HTMLElement;
|
|
40
|
+
export let chatPanelEl = null as unknown as HTMLElement;
|
|
40
41
|
export let toolbarEl = null as unknown as HTMLElement;
|
|
41
42
|
export let statusbarEl = null as unknown as HTMLElement;
|
|
43
|
+
export let frontmatterPanelEl = null as unknown as HTMLElement;
|
|
42
44
|
|
|
43
45
|
export function initShellRefs() {
|
|
44
46
|
canvasWrap = document.querySelector("#canvas-wrap") as HTMLElement;
|
|
45
47
|
activityBar = document.querySelector("#activity-bar") as HTMLElement;
|
|
46
48
|
leftPanel = document.querySelector("#left-panel") as HTMLElement;
|
|
47
49
|
rightPanel = document.querySelector("#right-panel") as HTMLElement;
|
|
50
|
+
chatPanelEl = document.querySelector("#chat-panel") as HTMLElement;
|
|
48
51
|
toolbarEl = document.querySelector("#toolbar") as HTMLElement;
|
|
49
52
|
statusbarEl = document.querySelector("#statusbar") as HTMLElement;
|
|
53
|
+
frontmatterPanelEl = document.querySelector("#frontmatter-panel") as HTMLElement;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// ─── Shared containers (mutated in place by owner modules) ───────────────────
|
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,
|
|
@@ -63,6 +70,7 @@ import {
|
|
|
63
70
|
documentExtensions,
|
|
64
71
|
formatForPath,
|
|
65
72
|
loadFormats,
|
|
73
|
+
refreshExtensionUi,
|
|
66
74
|
refreshFormats,
|
|
67
75
|
} from "./format/format-host";
|
|
68
76
|
import {
|
|
@@ -110,12 +118,17 @@ import { cloneRepository, renderGitPanel } from "./panels/git-panel";
|
|
|
110
118
|
// By Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
111
119
|
import { components as _swc } from "./ui/spectrum";
|
|
112
120
|
import "./ui/panel-resize.js";
|
|
113
|
-
|
|
121
|
+
// Built-in schema-form controls (schema-builder, secret) register on import
|
|
122
|
+
import "./ui/form-controls.js";
|
|
123
|
+
import { initLayers, showSaveDiscardDialog } from "./ui/layers";
|
|
114
124
|
import { initShortcuts } from "./editor/shortcuts";
|
|
115
125
|
import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar";
|
|
116
126
|
import * as toolbarPanel from "./panels/toolbar";
|
|
117
127
|
import * as overlaysPanel from "./panels/overlays";
|
|
128
|
+
import * as frontmatterPanelMod from "./panels/frontmatter-panel";
|
|
118
129
|
import * as rightPanelMod from "./panels/right-panel";
|
|
130
|
+
import * as chatPanelMod from "./panels/chat-panel";
|
|
131
|
+
import { setProjectAdopter } from "./services/project-adoption";
|
|
119
132
|
import * as leftPanelMod from "./panels/left-panel";
|
|
120
133
|
import * as tabStrip from "./panels/tab-strip";
|
|
121
134
|
import * as tabBar from "./panels/tab-bar";
|
|
@@ -124,6 +137,7 @@ import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from ".
|
|
|
124
137
|
import { registerCanvasDndBridge } from "./panels/canvas-dnd-bridge";
|
|
125
138
|
import { defaultDef } from "./panels/shared";
|
|
126
139
|
import { registerFunctionCompletions } from "./panels/editors";
|
|
140
|
+
import { closeFormulaWorkspace } from "./panels/formula-workspace";
|
|
127
141
|
import {
|
|
128
142
|
initBlockActionBar,
|
|
129
143
|
isEditChromeTarget,
|
|
@@ -131,12 +145,15 @@ import {
|
|
|
131
145
|
} from "./panels/block-action-bar";
|
|
132
146
|
import { initCssData } from "./panels/style-utils";
|
|
133
147
|
import { initQuickSearch } from "./panels/quick-search";
|
|
148
|
+
import { hydrateAccountStatus } from "./account-status";
|
|
134
149
|
import { hydrateProjectList } from "./project-list";
|
|
135
150
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
136
151
|
import { hydrateSettings } from "./services/settings-store";
|
|
137
152
|
import { initWelcome } from "./panels/welcome-screen";
|
|
153
|
+
import { openAddRepoModal } from "./new-project/add-repo-modal";
|
|
138
154
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
139
155
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
156
|
+
import type { Tab } from "./tabs/tab";
|
|
140
157
|
import type { JxPath } from "./state";
|
|
141
158
|
import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
142
159
|
|
|
@@ -218,19 +235,41 @@ async function navigateToComponent(componentPath: string) {
|
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Leaving a drilled-in component discards its edits (the pop restores the parent). Because saving
|
|
240
|
+
* is explicit, prompt when the child is dirty: Save writes it, Discard drops it, Cancel stays.
|
|
241
|
+
* Returns false to abort navigation.
|
|
242
|
+
*
|
|
243
|
+
* @param {Tab} tab
|
|
244
|
+
*/
|
|
245
|
+
async function confirmLeaveDirtyChild(tab: Tab): Promise<boolean> {
|
|
246
|
+
if (!tab.doc.dirty || !tab.documentPath) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
const name = tab.documentPath.split("/").pop() || "component";
|
|
250
|
+
const choice = await showSaveDiscardDialog("Unsaved Changes", `"${name}" has unsaved changes.`);
|
|
251
|
+
if (choice === "cancel") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (choice === "save") {
|
|
255
|
+
try {
|
|
256
|
+
await getPlatform().writeFile(tab.documentPath, await serializeDocument(tab));
|
|
257
|
+
} catch (error) {
|
|
258
|
+
statusMessage(`Save error: ${(error as Error).message}`);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// "discard": leave without writing — the child's edits are dropped with the popped frame.
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
221
266
|
async function navigateBack() {
|
|
222
267
|
const tab = activeTab.value;
|
|
223
268
|
if (!tab?.session.documentStack || tab.session.documentStack.length === 0) {
|
|
224
269
|
return;
|
|
225
270
|
}
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
const platform = getPlatform();
|
|
229
|
-
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
230
|
-
} catch (error) {
|
|
231
|
-
const err = error as Error;
|
|
232
|
-
statusMessage(`Save error: ${err.message}`);
|
|
233
|
-
}
|
|
271
|
+
if (!(await confirmLeaveDirtyChild(tab))) {
|
|
272
|
+
return;
|
|
234
273
|
}
|
|
235
274
|
|
|
236
275
|
// Pop the stack
|
|
@@ -257,14 +296,8 @@ async function navigateToLevel(targetIndex: number) {
|
|
|
257
296
|
if (!stack || targetIndex < 0 || targetIndex >= stack.length) {
|
|
258
297
|
return;
|
|
259
298
|
}
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
const platform = getPlatform();
|
|
263
|
-
await platform.writeFile(tab.documentPath, await serializeDocument(tab));
|
|
264
|
-
} catch (error) {
|
|
265
|
-
const err = error as Error;
|
|
266
|
-
statusMessage(`Save error: ${err.message}`);
|
|
267
|
-
}
|
|
299
|
+
if (!(await confirmLeaveDirtyChild(tab))) {
|
|
300
|
+
return;
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
const frame = stack[targetIndex] as DocumentStackEntry;
|
|
@@ -388,6 +421,7 @@ initQuickSearch({ openRecentProject: (root: string) => openRecentProject(root) }
|
|
|
388
421
|
tabStrip.mount(document.querySelector("#tab-strip") as HTMLElement);
|
|
389
422
|
|
|
390
423
|
tabBar.mount(document.querySelector("#tab-bar") as HTMLElement, {
|
|
424
|
+
closeFormulaWorkspace: () => closeFormulaWorkspace(),
|
|
391
425
|
closeFunctionEditor: () => closeFunctionEditor(),
|
|
392
426
|
exportFile,
|
|
393
427
|
getCanvasMode,
|
|
@@ -437,6 +471,26 @@ document.addEventListener(
|
|
|
437
471
|
true,
|
|
438
472
|
);
|
|
439
473
|
|
|
474
|
+
// Unsaved-changes guard: saving is explicit (no idle autosave), so warn before the window unloads
|
|
475
|
+
// While any open tab has unsaved edits. For collab tabs, `dirty` reflects the room-level unsaved
|
|
476
|
+
// State; closing loses in-memory edits that were never flushed to disk.
|
|
477
|
+
export function hasUnsavedTabs(): boolean {
|
|
478
|
+
for (const tab of workspace.tabs.values()) {
|
|
479
|
+
if (tab.doc.dirty) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
window.addEventListener("beforeunload", (e: BeforeUnloadEvent) => {
|
|
487
|
+
if (hasUnsavedTabs()) {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
// Legacy browsers require a truthy returnValue to trigger the native confirm prompt.
|
|
490
|
+
e.returnValue = "";
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
440
494
|
initCanvasUtils({
|
|
441
495
|
getCanvasMode,
|
|
442
496
|
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
@@ -471,6 +525,16 @@ initCanvasRender({
|
|
|
471
525
|
});
|
|
472
526
|
|
|
473
527
|
initWelcome({
|
|
528
|
+
addExistingRepo: async () => {
|
|
529
|
+
const result = await openAddRepoModal();
|
|
530
|
+
if (result) {
|
|
531
|
+
// The catalogue gained an entry; refresh it before navigating into the project.
|
|
532
|
+
void hydrateProjectList().then(() => {
|
|
533
|
+
render();
|
|
534
|
+
});
|
|
535
|
+
void openRecentProject(result.root);
|
|
536
|
+
}
|
|
537
|
+
},
|
|
474
538
|
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
475
539
|
openNewProject: async () => {
|
|
476
540
|
const result = await openNewProjectModal();
|
|
@@ -503,10 +567,12 @@ effect(() => {
|
|
|
503
567
|
if (tab) {
|
|
504
568
|
void tab.doc.mode;
|
|
505
569
|
void tab.session.ui.canvasMode;
|
|
570
|
+
void tab.session.ui.editingFormula;
|
|
506
571
|
void tab.session.ui.editingFunction;
|
|
507
572
|
void tab.session.ui.featureToggles;
|
|
508
573
|
void tab.session.ui.preview;
|
|
509
574
|
void tab.session.ui.previewParams;
|
|
575
|
+
void tab.session.ui.previewProps;
|
|
510
576
|
void tab.session.ui.settingsTab;
|
|
511
577
|
void tab.session.ui.showLayout;
|
|
512
578
|
void tab.session.ui.stylebookTab;
|
|
@@ -522,6 +588,15 @@ rightPanelMod.mount({
|
|
|
522
588
|
renderCanvas: () => renderCanvas(),
|
|
523
589
|
});
|
|
524
590
|
|
|
591
|
+
// Above-canvas frontmatter Properties panel (content-collection docs, edit mode).
|
|
592
|
+
frontmatterPanelMod.mount({ getCanvasMode });
|
|
593
|
+
|
|
594
|
+
// The persistent AI chat sidebar — mounts once, available with or without a project/document.
|
|
595
|
+
chatPanelMod.mount(chatPanelEl);
|
|
596
|
+
// The assistant's create_project tool adopts freshly scaffolded projects through the same
|
|
597
|
+
// Flow as the recent-projects list.
|
|
598
|
+
setProjectAdopter(openRecentProject);
|
|
599
|
+
|
|
525
600
|
leftPanelMod.mount({
|
|
526
601
|
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
527
602
|
defBadgeLabel,
|
|
@@ -553,6 +628,8 @@ leftPanelMod.mount({
|
|
|
553
628
|
registerRenderer("leftPanel", () => leftPanelMod.render());
|
|
554
629
|
registerRenderer("canvas", () => renderCanvas());
|
|
555
630
|
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
631
|
+
registerRenderer("frontmatterPanel", () => frontmatterPanelMod.render());
|
|
632
|
+
registerRenderer("chatPanel", () => chatPanelMod.render());
|
|
556
633
|
registerRenderer("overlays", () => overlaysPanel.render());
|
|
557
634
|
setStatusbarRenderer(() => renderStatusbar());
|
|
558
635
|
mountStatusbar();
|
|
@@ -652,7 +729,9 @@ if (_projectParam) {
|
|
|
652
729
|
searchQuery: "",
|
|
653
730
|
selectedPath: siteCtx.fileRelPath || null,
|
|
654
731
|
});
|
|
732
|
+
setWorkspaceProject(siteCtx.sitePath, siteCtx.projectConfig || null);
|
|
655
733
|
|
|
734
|
+
refreshExtensionUi(platform);
|
|
656
735
|
await autoSyncProjectOnOpen();
|
|
657
736
|
await ensureDependenciesInstalled();
|
|
658
737
|
await loadComponentRegistry();
|
|
@@ -769,6 +848,13 @@ void hydrateProjectList().then(() => {
|
|
|
769
848
|
render();
|
|
770
849
|
});
|
|
771
850
|
|
|
851
|
+
// Hydrate the account onboarding status (GitHub-App installation coverage on cloud), then refresh
|
|
852
|
+
// The welcome screen's install prompt. No-op on platforms without getAccountStatus.
|
|
853
|
+
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
|
|
854
|
+
void hydrateAccountStatus().then(() => {
|
|
855
|
+
render();
|
|
856
|
+
});
|
|
857
|
+
|
|
772
858
|
// Hydrate user settings (AI connection parameters) from the backend store, then re-render so
|
|
773
859
|
// Key-gated surfaces (assistant gate, New Project Import/Agent tabs) see the stored key.
|
|
774
860
|
// oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
|
|
@@ -819,6 +905,7 @@ async function openRecentProject(root: string) {
|
|
|
819
905
|
// "No format class imported" until a reload. Mirrors openProject in files.ts.
|
|
820
906
|
refreshFormats();
|
|
821
907
|
void loadFormats();
|
|
908
|
+
refreshExtensionUi(platform);
|
|
822
909
|
const content = await platform.readFile("project.json");
|
|
823
910
|
const config = JSON.parse(content) as ProjectConfig;
|
|
824
911
|
|
|
@@ -835,6 +922,7 @@ async function openRecentProject(root: string) {
|
|
|
835
922
|
searchQuery: "",
|
|
836
923
|
selectedPath: null,
|
|
837
924
|
});
|
|
925
|
+
setWorkspaceProject(root, config);
|
|
838
926
|
|
|
839
927
|
await autoSyncProjectOnOpen();
|
|
840
928
|
await ensureDependenciesInstalled();
|
|
@@ -894,7 +982,6 @@ initShortcuts(() => ({
|
|
|
894
982
|
openProject,
|
|
895
983
|
panX: view.panX,
|
|
896
984
|
panY: view.panY,
|
|
897
|
-
positionZoomIndicator,
|
|
898
985
|
saveFile,
|
|
899
986
|
setPan: (x, y) => {
|
|
900
987
|
view.panX = x;
|
|
@@ -902,35 +989,3 @@ initShortcuts(() => ({
|
|
|
902
989
|
view.needsCenter = false;
|
|
903
990
|
},
|
|
904
991
|
}));
|
|
905
|
-
|
|
906
|
-
// ─── Autosave (registered as update middleware) ──────────────────────────────
|
|
907
|
-
|
|
908
|
-
const AUTO_SAVE_DELAY = 2000;
|
|
909
|
-
|
|
910
|
-
function scheduleAutosave() {
|
|
911
|
-
const tab = activeTab.value;
|
|
912
|
-
if (!tab?.fileHandle || !tab.doc.dirty) {
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (view.autosaveTimer) {
|
|
916
|
-
clearTimeout(view.autosaveTimer);
|
|
917
|
-
}
|
|
918
|
-
view.autosaveTimer = setTimeout(async () => {
|
|
919
|
-
const t = activeTab.value;
|
|
920
|
-
if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
|
|
921
|
-
try {
|
|
922
|
-
const writable = await t.fileHandle.createWritable();
|
|
923
|
-
await writable.write(await serializeDocument(t));
|
|
924
|
-
await writable.close();
|
|
925
|
-
t.doc.dirty = false;
|
|
926
|
-
statusMessage("Auto-saved");
|
|
927
|
-
} catch {}
|
|
928
|
-
}
|
|
929
|
-
}, AUTO_SAVE_DELAY);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
effect(() => {
|
|
933
|
-
if (activeTab.value?.doc.dirty) {
|
|
934
|
-
scheduleAutosave();
|
|
935
|
-
}
|
|
936
|
-
});
|
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
|
}
|