@jxsuite/studio 0.11.0 → 0.14.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/studio.js +6517 -5870
- package/dist/studio.js.map +57 -51
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +28 -2
- package/src/canvas/canvas-render.js +170 -20
- package/src/canvas/canvas-utils.js +22 -25
- package/src/editor/component-inline-edit.js +55 -44
- package/src/editor/content-inline-edit.js +47 -50
- package/src/editor/context-menu.js +78 -53
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -11
- package/src/editor/shortcuts.js +88 -64
- package/src/files/components.js +15 -4
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +81 -28
- package/src/panels/activity-bar.js +31 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +77 -35
- package/src/panels/editors.js +17 -26
- package/src/panels/elements-panel.js +17 -11
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +47 -4
- package/src/panels/layers-panel.js +25 -21
- package/src/panels/left-panel.js +109 -44
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +28 -37
- package/src/panels/preview-render.js +7 -3
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/pseudo-preview.js +9 -8
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +26 -19
- package/src/panels/style-inputs.js +5 -4
- package/src/panels/style-panel.js +128 -105
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +27 -31
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +40 -10
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +2 -459
- package/src/store.js +61 -219
- package/src/studio.js +163 -300
- package/src/tabs/tab.js +168 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +90 -0
package/src/files/components.js
CHANGED
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
import { getPlatform } from "../platform.js";
|
|
4
4
|
import { projectState } from "../store.js";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* tagName: string;
|
|
9
|
+
* path: string;
|
|
10
|
+
* props?: { name: string; type?: string; [k: string]: any }[];
|
|
11
|
+
* source?: string;
|
|
12
|
+
* package?: string;
|
|
13
|
+
* modulePath?: string;
|
|
14
|
+
* }} ComponentEntry
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {ComponentEntry[]} */
|
|
18
|
+
export let componentRegistry = [];
|
|
8
19
|
export let _componentRegistryLoaded = false;
|
|
9
20
|
|
|
10
21
|
export async function loadComponentRegistry() {
|
|
@@ -18,8 +29,8 @@ export async function loadComponentRegistry() {
|
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {
|
|
32
|
+
* @param {string | null} fromDocPath
|
|
33
|
+
* @param {string} toCompPath
|
|
23
34
|
*/
|
|
24
35
|
export function computeRelativePath(fromDocPath, toCompPath) {
|
|
25
36
|
if (!fromDocPath) return `./${toCompPath}`;
|
package/src/files/file-ops.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File Operations — open,
|
|
2
|
+
* File Operations — open, save, export documents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* can assign S and trigger render().
|
|
4
|
+
* All functions read/write directly from/to `activeTab.value` (the reactive tab).
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { unified } from "unified";
|
|
@@ -10,17 +9,13 @@ import remarkStringify from "remark-stringify";
|
|
|
10
9
|
import remarkDirective from "remark-directive";
|
|
11
10
|
import { stringify as stringifyYaml } from "yaml";
|
|
12
11
|
import { jxToMd, jxDocToMd } from "../markdown/md-convert.js";
|
|
13
|
-
import { createState } from "../store.js";
|
|
14
12
|
import { locateDocument } from "../services/code-services.js";
|
|
15
13
|
import { statusMessage } from "../panels/statusbar.js";
|
|
16
14
|
import { getPlatform } from "../platform.js";
|
|
15
|
+
import { activeTab, openTab } from "../workspace/workspace.js";
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
* @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
|
|
22
|
-
*/
|
|
23
|
-
export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar }) {
|
|
17
|
+
/** Open a file via the File System Access API (or fallback input). */
|
|
18
|
+
export async function openFile() {
|
|
24
19
|
try {
|
|
25
20
|
if ("showOpenFilePicker" in window) {
|
|
26
21
|
const [handle] = await /** @type {any} */ (window).showOpenFilePicker({
|
|
@@ -31,18 +26,21 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
31
26
|
});
|
|
32
27
|
const file = await handle.getFile();
|
|
33
28
|
const text = await file.text();
|
|
29
|
+
const documentPath = await locateDocument(handle.name);
|
|
34
30
|
|
|
35
31
|
if (handle.name.endsWith(".md")) {
|
|
36
|
-
const
|
|
37
|
-
|
|
32
|
+
const { document, frontmatter } = await loadMarkdown(text);
|
|
33
|
+
openTab({
|
|
34
|
+
id: handle.name,
|
|
35
|
+
documentPath,
|
|
36
|
+
fileHandle: handle,
|
|
37
|
+
document,
|
|
38
|
+
frontmatter,
|
|
39
|
+
sourceFormat: "md",
|
|
40
|
+
});
|
|
38
41
|
} else {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
newState.fileHandle = handle;
|
|
42
|
-
newState.dirty = false;
|
|
43
|
-
newState.documentPath = await locateDocument(handle.name);
|
|
44
|
-
await loadCompanionJS(handle, newState);
|
|
45
|
-
commit(newState);
|
|
42
|
+
const document = JSON.parse(text);
|
|
43
|
+
openTab({ id: handle.name, documentPath, fileHandle: handle, document });
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
statusMessage(`Opened ${handle.name}`);
|
|
@@ -57,13 +55,11 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
57
55
|
const text = await file.text();
|
|
58
56
|
|
|
59
57
|
if (file.name.endsWith(".md")) {
|
|
60
|
-
const
|
|
61
|
-
|
|
58
|
+
const { document, frontmatter } = await loadMarkdown(text);
|
|
59
|
+
openTab({ id: file.name, document, frontmatter, sourceFormat: "md" });
|
|
62
60
|
} else {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
newState.dirty = false;
|
|
66
|
-
commit(newState);
|
|
61
|
+
const document = JSON.parse(text);
|
|
62
|
+
openTab({ id: file.name, document });
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
statusMessage(`Opened ${file.name}`);
|
|
@@ -76,93 +72,50 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/**
|
|
79
|
-
* Parse a markdown string into
|
|
75
|
+
* Parse a markdown string into document + frontmatter (pure — no side effects).
|
|
80
76
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* contract.
|
|
84
|
-
*
|
|
85
|
-
* @param {any} source Markdown text
|
|
86
|
-
* @param {any} fileHandle File handle (or null)
|
|
87
|
-
* @returns {Promise<any>} A new state object ready for commit()
|
|
77
|
+
* @param {string} source Markdown text
|
|
78
|
+
* @returns {Promise<{ document: any; frontmatter: Record<string, any> }>}
|
|
88
79
|
*/
|
|
89
|
-
export async function loadMarkdown(source
|
|
80
|
+
export async function loadMarkdown(source) {
|
|
90
81
|
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
91
82
|
const doc = /** @type {any} */ (transpileJxMarkdown(source));
|
|
92
83
|
|
|
93
84
|
const isComponent = doc.tagName && String(doc.tagName).includes("-");
|
|
94
85
|
|
|
95
86
|
if (isComponent) {
|
|
96
|
-
|
|
97
|
-
newState.sourceFormat = "md";
|
|
98
|
-
newState.rawMarkdown = source;
|
|
99
|
-
newState.fileHandle = fileHandle;
|
|
100
|
-
newState.dirty = false;
|
|
101
|
-
return newState;
|
|
87
|
+
return { document: doc, frontmatter: {} };
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
// Content markdown — children form the root-level document body
|
|
105
|
-
const contentDoc = {
|
|
106
|
-
children: doc.children ?? [],
|
|
107
|
-
};
|
|
91
|
+
const contentDoc = { children: doc.children ?? [] };
|
|
108
92
|
|
|
109
|
-
// Extract frontmatter keys (everything except children) as content metadata
|
|
110
93
|
/** @type {Record<string, any>} */
|
|
111
94
|
const frontmatter = {};
|
|
112
95
|
for (const [key, value] of Object.entries(doc)) {
|
|
113
96
|
if (key !== "children") frontmatter[key] = value;
|
|
114
97
|
}
|
|
115
98
|
|
|
116
|
-
|
|
117
|
-
newState.sourceFormat = "md";
|
|
118
|
-
newState.mode = "content";
|
|
119
|
-
newState.content = { frontmatter };
|
|
120
|
-
newState.rawMarkdown = source;
|
|
121
|
-
newState.fileHandle = fileHandle;
|
|
122
|
-
newState.dirty = false;
|
|
123
|
-
return newState;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Load companion JS file metadata into state.
|
|
128
|
-
*
|
|
129
|
-
* @param {any} handle
|
|
130
|
-
* @param {any} state State object to mutate in-place
|
|
131
|
-
*/
|
|
132
|
-
async function loadCompanionJS(handle, state) {
|
|
133
|
-
try {
|
|
134
|
-
if (handle.getParent) {
|
|
135
|
-
// Not yet available in any browser; skip for now
|
|
136
|
-
}
|
|
137
|
-
if (state.document.$handlers) {
|
|
138
|
-
state.handlersSource = `// Companion file: ${state.document.$handlers}\n// (Read-only in builder — edit the JS file directly)`;
|
|
139
|
-
}
|
|
140
|
-
} catch {}
|
|
99
|
+
return { document: contentDoc, frontmatter };
|
|
141
100
|
}
|
|
142
101
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*/
|
|
148
|
-
export async function saveFile({ S, commit, renderToolbar }) {
|
|
102
|
+
/** Save the current document back to its source location. */
|
|
103
|
+
export async function saveFile() {
|
|
104
|
+
const tab = activeTab.value;
|
|
105
|
+
if (!tab) return;
|
|
149
106
|
try {
|
|
150
|
-
const output = serializeDocument(
|
|
107
|
+
const output = serializeDocument(tab);
|
|
151
108
|
|
|
152
|
-
if (
|
|
153
|
-
// Project file — save via platform
|
|
109
|
+
if (tab.documentPath) {
|
|
154
110
|
const platform = getPlatform();
|
|
155
|
-
await platform.writeFile(
|
|
156
|
-
|
|
157
|
-
renderToolbar();
|
|
111
|
+
await platform.writeFile(tab.documentPath, output);
|
|
112
|
+
tab.doc.dirty = false;
|
|
158
113
|
statusMessage("Saved");
|
|
159
|
-
} else if (
|
|
160
|
-
|
|
161
|
-
const writable = await S.fileHandle.createWritable();
|
|
114
|
+
} else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
|
|
115
|
+
const writable = await /** @type {any} */ (tab.fileHandle).createWritable();
|
|
162
116
|
await writable.write(output);
|
|
163
117
|
await writable.close();
|
|
164
|
-
|
|
165
|
-
renderToolbar();
|
|
118
|
+
tab.doc.dirty = false;
|
|
166
119
|
statusMessage("Saved");
|
|
167
120
|
} else {
|
|
168
121
|
statusMessage("No save target — use Export");
|
|
@@ -172,22 +125,20 @@ export async function saveFile({ S, commit, renderToolbar }) {
|
|
|
172
125
|
}
|
|
173
126
|
}
|
|
174
127
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*/
|
|
180
|
-
export async function exportFile({ S, commit, renderToolbar }) {
|
|
128
|
+
/** Export the current document to a new location (Save As / download). */
|
|
129
|
+
export async function exportFile() {
|
|
130
|
+
const tab = activeTab.value;
|
|
131
|
+
if (!tab) return;
|
|
181
132
|
try {
|
|
182
|
-
const isContent =
|
|
183
|
-
const output = serializeDocument(
|
|
133
|
+
const isContent = tab.doc.mode === "content";
|
|
134
|
+
const output = serializeDocument(tab);
|
|
184
135
|
const mimeType = isContent ? "text/markdown" : "application/json";
|
|
185
136
|
const ext = isContent ? ".md" : ".json";
|
|
186
137
|
const description = isContent ? "Markdown Content" : "Jx Component";
|
|
187
138
|
|
|
188
139
|
if ("showSaveFilePicker" in window) {
|
|
189
|
-
const suggestedName =
|
|
190
|
-
?
|
|
140
|
+
const suggestedName = tab.documentPath
|
|
141
|
+
? tab.documentPath.split("/").pop()
|
|
191
142
|
: isContent
|
|
192
143
|
? "content.md"
|
|
193
144
|
: "component.json";
|
|
@@ -198,8 +149,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
198
149
|
const writable = await handle.createWritable();
|
|
199
150
|
await writable.write(output);
|
|
200
151
|
await writable.close();
|
|
201
|
-
|
|
202
|
-
renderToolbar();
|
|
152
|
+
tab.doc.dirty = false;
|
|
203
153
|
statusMessage(`Exported as ${handle.name}`);
|
|
204
154
|
} else {
|
|
205
155
|
// Fallback: download
|
|
@@ -210,8 +160,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
210
160
|
a.download = isContent ? "content.md" : "component.json";
|
|
211
161
|
a.click();
|
|
212
162
|
URL.revokeObjectURL(url);
|
|
213
|
-
|
|
214
|
-
renderToolbar();
|
|
163
|
+
tab.doc.dirty = false;
|
|
215
164
|
statusMessage("Downloaded");
|
|
216
165
|
}
|
|
217
166
|
} catch (/** @type {any} */ e) {
|
|
@@ -222,22 +171,22 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
222
171
|
/**
|
|
223
172
|
* Serialize the current document to its output format (JSON or Markdown).
|
|
224
173
|
*
|
|
225
|
-
* @param {
|
|
174
|
+
* @param {import("../tabs/tab.js").Tab} tab
|
|
226
175
|
* @returns {string}
|
|
227
176
|
*/
|
|
228
|
-
function serializeDocument(
|
|
229
|
-
if (
|
|
230
|
-
return jxDocToMd(
|
|
177
|
+
function serializeDocument(tab) {
|
|
178
|
+
if (tab.doc.sourceFormat === "md") {
|
|
179
|
+
return jxDocToMd(tab.doc.document);
|
|
231
180
|
}
|
|
232
|
-
if (
|
|
233
|
-
const mdast = jxToMd(
|
|
181
|
+
if (tab.doc.mode === "content") {
|
|
182
|
+
const mdast = jxToMd(tab.doc.document);
|
|
234
183
|
const md = unified()
|
|
235
184
|
.use(remarkDirective)
|
|
236
185
|
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
237
186
|
.stringify(mdast);
|
|
238
|
-
const fm =
|
|
187
|
+
const fm = tab.doc.content?.frontmatter;
|
|
239
188
|
const hasFrontmatter = fm && Object.keys(fm).length > 0;
|
|
240
189
|
return hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
|
|
241
190
|
}
|
|
242
|
-
return JSON.stringify(
|
|
191
|
+
return JSON.stringify(tab.doc.document, null, 2);
|
|
243
192
|
}
|
package/src/files/files.js
CHANGED
|
@@ -15,10 +15,13 @@ import { createState, projectState, setProjectState } from "../store.js";
|
|
|
15
15
|
import { getPlatform } from "../platform.js";
|
|
16
16
|
import { statusMessage } from "../panels/statusbar.js";
|
|
17
17
|
import { loadComponentRegistry } from "./components.js";
|
|
18
|
+
import { workspace, openTab, activateTab } from "../workspace/workspace.js";
|
|
19
|
+
import { loadMarkdown } from "./file-ops.js";
|
|
20
|
+
import { view } from "../view.js";
|
|
18
21
|
|
|
19
22
|
// ─── File icon map ────────────────────────────────────────────────────────────
|
|
20
23
|
|
|
21
|
-
const fileIconMap = /** @type {Record<string,
|
|
24
|
+
const fileIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
|
|
22
25
|
"sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
|
|
23
26
|
"sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
|
|
24
27
|
"sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
|
|
@@ -29,7 +32,7 @@ const fileIconMap = /** @type {Record<string, any>} */ ({
|
|
|
29
32
|
|
|
30
33
|
// ─── File management ──────────────────────────────────────────────────────────
|
|
31
34
|
|
|
32
|
-
async function loadDirectory(/** @type {
|
|
35
|
+
async function loadDirectory(/** @type {string} */ dirPath) {
|
|
33
36
|
if (!projectState) return;
|
|
34
37
|
try {
|
|
35
38
|
const platform = getPlatform();
|
|
@@ -77,13 +80,11 @@ export async function loadProject() {
|
|
|
77
80
|
* Open a project via the platform adapter.
|
|
78
81
|
*
|
|
79
82
|
* @param {{
|
|
80
|
-
* S: any;
|
|
81
|
-
* commit: (s: any) => void;
|
|
82
83
|
* renderActivityBar: () => void;
|
|
83
84
|
* renderLeftPanel: () => void;
|
|
84
85
|
* }} ctx
|
|
85
86
|
*/
|
|
86
|
-
export async function openProject({
|
|
87
|
+
export async function openProject({ renderActivityBar, renderLeftPanel }) {
|
|
87
88
|
try {
|
|
88
89
|
const platform = getPlatform();
|
|
89
90
|
const result = await platform.openProject();
|
|
@@ -127,7 +128,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
|
|
|
127
128
|
}
|
|
128
129
|
projectState.projectDirs = foundDirs;
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
view.leftTab = "files";
|
|
131
132
|
renderActivityBar();
|
|
132
133
|
renderLeftPanel();
|
|
133
134
|
statusMessage(`Opened project: ${projectState.name}`);
|
|
@@ -138,7 +139,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
|
|
|
138
139
|
|
|
139
140
|
// ─── File tree templates ──────────────────────────────────────────────────────
|
|
140
141
|
|
|
141
|
-
function fileTypeIconTpl(/** @type {
|
|
142
|
+
function fileTypeIconTpl(/** @type {string} */ name, /** @type {string} */ type) {
|
|
142
143
|
let tag;
|
|
143
144
|
if (type === "directory") {
|
|
144
145
|
tag = projectState?.expanded?.has(name) ? "sp-icon-folder-open" : "sp-icon-folder";
|
|
@@ -237,11 +238,11 @@ export function renderFilesTemplate({
|
|
|
237
238
|
quiet
|
|
238
239
|
placeholder="Filter files…"
|
|
239
240
|
value=${projectState.searchQuery}
|
|
240
|
-
@input=${(/** @type {
|
|
241
|
-
projectState.searchQuery = e.target.value;
|
|
241
|
+
@input=${(/** @type {Event} */ e) => {
|
|
242
|
+
projectState.searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
|
|
242
243
|
renderLeftPanel();
|
|
243
244
|
}}
|
|
244
|
-
@submit=${(/** @type {
|
|
245
|
+
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
245
246
|
></sp-search>
|
|
246
247
|
</div>
|
|
247
248
|
<div class="file-tree" role="tree" aria-label="Project files">
|
|
@@ -250,10 +251,10 @@ export function renderFilesTemplate({
|
|
|
250
251
|
`;
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
/** @returns {
|
|
254
|
+
/** @returns {import("lit-html").TemplateResult | import("lit-html").TemplateResult[]} */
|
|
254
255
|
function renderTreeLevelTemplate(
|
|
255
|
-
/** @type {
|
|
256
|
-
/** @type {
|
|
256
|
+
/** @type {string} */ dirPath,
|
|
257
|
+
/** @type {number} */ depth,
|
|
257
258
|
/** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
|
|
258
259
|
) {
|
|
259
260
|
const entries = projectState.dirs.get(dirPath);
|
|
@@ -293,7 +294,7 @@ function renderTreeLevelTemplate(
|
|
|
293
294
|
data-path=${entry.path}
|
|
294
295
|
data-type=${entry.type}
|
|
295
296
|
aria-expanded=${isDir ? String(isExpanded) : nothing}
|
|
296
|
-
@click=${async (/** @type {
|
|
297
|
+
@click=${async (/** @type {MouseEvent} */ e) => {
|
|
297
298
|
e.stopPropagation();
|
|
298
299
|
if (isDir) {
|
|
299
300
|
if (isExpanded) projectState.expanded.delete(entry.path);
|
|
@@ -306,7 +307,7 @@ function renderTreeLevelTemplate(
|
|
|
306
307
|
ctx.openFileFn(entry.path);
|
|
307
308
|
}
|
|
308
309
|
}}
|
|
309
|
-
@contextmenu=${(/** @type {
|
|
310
|
+
@contextmenu=${(/** @type {MouseEvent} */ e) => {
|
|
310
311
|
e.preventDefault();
|
|
311
312
|
e.stopPropagation();
|
|
312
313
|
showFileContextMenu(e, entry, ctx);
|
|
@@ -325,10 +326,10 @@ function renderTreeLevelTemplate(
|
|
|
325
326
|
});
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
export function setupTreeKeyboard(/** @type {
|
|
329
|
-
tree.addEventListener("keydown", (/** @type {
|
|
330
|
-
const items = [...tree.querySelectorAll(".file-tree-item")];
|
|
331
|
-
const focused = tree.querySelector(".file-tree-item:focus");
|
|
329
|
+
export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
|
|
330
|
+
tree.addEventListener("keydown", (/** @type {KeyboardEvent} */ e) => {
|
|
331
|
+
const items = /** @type {HTMLElement[]} */ ([...tree.querySelectorAll(".file-tree-item")]);
|
|
332
|
+
const focused = /** @type {HTMLElement | null} */ (tree.querySelector(".file-tree-item:focus"));
|
|
332
333
|
if (!focused || items.length === 0) return;
|
|
333
334
|
|
|
334
335
|
const idx = items.indexOf(focused);
|
|
@@ -343,12 +344,15 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
|
|
|
343
344
|
break;
|
|
344
345
|
case "ArrowRight":
|
|
345
346
|
if (focused.dataset.type === "directory") {
|
|
346
|
-
const path = focused.dataset.path;
|
|
347
|
+
const path = /** @type {string} */ (focused.dataset.path);
|
|
347
348
|
if (!projectState.expanded.has(path)) {
|
|
348
349
|
projectState.expanded.add(path);
|
|
349
350
|
loadDirectory(path).then(() => {
|
|
350
351
|
const panel = tree.closest(".panel-body");
|
|
351
|
-
if (panel)
|
|
352
|
+
if (panel)
|
|
353
|
+
/** @type {HTMLElement | null} */ (
|
|
354
|
+
panel.querySelector(".file-tree-item:focus")
|
|
355
|
+
)?.click();
|
|
352
356
|
});
|
|
353
357
|
}
|
|
354
358
|
}
|
|
@@ -378,12 +382,12 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
|
|
|
378
382
|
|
|
379
383
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
380
384
|
|
|
381
|
-
/** @type {
|
|
385
|
+
/** @type {HTMLElement | null} */
|
|
382
386
|
let fileContextPopover = null;
|
|
383
387
|
|
|
384
388
|
function showFileContextMenu(
|
|
385
|
-
/** @type {
|
|
386
|
-
/** @type {
|
|
389
|
+
/** @type {MouseEvent} */ e,
|
|
390
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
387
391
|
/** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
|
|
388
392
|
) {
|
|
389
393
|
if (fileContextPopover) {
|
|
@@ -442,8 +446,8 @@ function showFileContextMenu(
|
|
|
442
446
|
litRender(tpl, fileContextPopover);
|
|
443
447
|
document.body.appendChild(fileContextPopover);
|
|
444
448
|
|
|
445
|
-
const closeHandler = (/** @type {
|
|
446
|
-
if (!fileContextPopover?.contains(ev.target)) {
|
|
449
|
+
const closeHandler = (/** @type {MouseEvent} */ ev) => {
|
|
450
|
+
if (!fileContextPopover?.contains(/** @type {Node} */ (ev.target))) {
|
|
447
451
|
closeFileContextMenu();
|
|
448
452
|
document.removeEventListener("mousedown", closeHandler, true);
|
|
449
453
|
}
|
|
@@ -478,7 +482,10 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
|
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
484
|
|
|
481
|
-
async function renameFile(
|
|
485
|
+
async function renameFile(
|
|
486
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
487
|
+
/** @type {() => void} */ renderLeftPanel,
|
|
488
|
+
) {
|
|
482
489
|
const newName = prompt("New name:", entry.name);
|
|
483
490
|
if (!newName || newName === entry.name) return;
|
|
484
491
|
const entryPath = entry.path.replaceAll("\\", "/");
|
|
@@ -500,7 +507,10 @@ async function renameFile(/** @type {any} */ entry, /** @type {() => void} */ re
|
|
|
500
507
|
}
|
|
501
508
|
}
|
|
502
509
|
|
|
503
|
-
async function deleteFile(
|
|
510
|
+
async function deleteFile(
|
|
511
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
512
|
+
/** @type {() => void} */ renderLeftPanel,
|
|
513
|
+
) {
|
|
504
514
|
if (!confirm(`Delete "${entry.name}"?`)) return;
|
|
505
515
|
try {
|
|
506
516
|
const platform = getPlatform();
|
|
@@ -583,3 +593,46 @@ export async function openFileFromTree(ctx, path) {
|
|
|
583
593
|
statusMessage(`Error: ${e.message}`);
|
|
584
594
|
}
|
|
585
595
|
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Open a file from the tree into a tab. Activates existing tab if already open.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} path
|
|
601
|
+
*/
|
|
602
|
+
export async function openFileInTab(path) {
|
|
603
|
+
for (const [id, tab] of workspace.tabs.entries()) {
|
|
604
|
+
if (tab.documentPath === path) {
|
|
605
|
+
activateTab(id);
|
|
606
|
+
projectState.selectedPath = path;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const platform = getPlatform();
|
|
612
|
+
try {
|
|
613
|
+
const content = await platform.readFile(path);
|
|
614
|
+
if (!content) return;
|
|
615
|
+
|
|
616
|
+
let document, frontmatter;
|
|
617
|
+
if (path.endsWith(".md")) {
|
|
618
|
+
const result = await loadMarkdown(content);
|
|
619
|
+
document = result.document;
|
|
620
|
+
frontmatter = result.frontmatter;
|
|
621
|
+
} else {
|
|
622
|
+
document = JSON.parse(content);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const id = path;
|
|
626
|
+
openTab({
|
|
627
|
+
id,
|
|
628
|
+
documentPath: path,
|
|
629
|
+
document,
|
|
630
|
+
frontmatter,
|
|
631
|
+
sourceFormat: path.endsWith(".md") ? "md" : null,
|
|
632
|
+
});
|
|
633
|
+
projectState.selectedPath = path;
|
|
634
|
+
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
635
|
+
} catch (/** @type {any} */ e) {
|
|
636
|
+
statusMessage(`Error: ${e.message}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
/** Activity bar — tab icons for switching left panel views. */
|
|
2
2
|
|
|
3
3
|
import { html, render as litRender, nothing } from "lit-html";
|
|
4
|
-
import { activityBar,
|
|
4
|
+
import { activityBar, renderOnly } from "../store.js";
|
|
5
|
+
import { effect, effectScope } from "../reactivity.js";
|
|
6
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
7
|
+
import { view } from "../view.js";
|
|
8
|
+
|
|
9
|
+
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
10
|
+
let _scope = null;
|
|
11
|
+
|
|
12
|
+
export function mount() {
|
|
13
|
+
_scope = effectScope();
|
|
14
|
+
_scope.run(() => {
|
|
15
|
+
effect(() => {
|
|
16
|
+
const tab = activeTab.value;
|
|
17
|
+
if (!tab) return;
|
|
18
|
+
renderActivityBar();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function unmount() {
|
|
24
|
+
_scope?.stop();
|
|
25
|
+
_scope = null;
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
const gitBranchIcon = (/** @type {any} */ s) => html`
|
|
7
29
|
<svg
|
|
@@ -58,8 +80,10 @@ export function tabIcon(tag, size) {
|
|
|
58
80
|
return fn ? fn(size || "s") : nothing;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
export function renderActivityBar() {
|
|
84
|
+
const tab = activeTab.value;
|
|
85
|
+
if (!tab) return;
|
|
86
|
+
const leftTab = view.leftTab;
|
|
63
87
|
const tabs = [
|
|
64
88
|
{ value: "files", icon: "sp-icon-folder", label: "Files" },
|
|
65
89
|
{ value: "layers", icon: "sp-icon-layers", label: "Layers" },
|
|
@@ -72,13 +96,13 @@ export function renderActivityBar(S) {
|
|
|
72
96
|
];
|
|
73
97
|
const tpl = html`
|
|
74
98
|
<sp-tabs
|
|
75
|
-
selected=${
|
|
99
|
+
selected=${leftTab}
|
|
76
100
|
direction="vertical"
|
|
77
101
|
quiet
|
|
78
102
|
@change=${(/** @type {any} */ e) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
view.leftTab = e.target.selected;
|
|
104
|
+
renderOnly("leftPanel");
|
|
105
|
+
renderActivityBar();
|
|
82
106
|
}}
|
|
83
107
|
>
|
|
84
108
|
${tabs.map(
|