@jxsuite/studio 0.10.2 → 0.13.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/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- 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 +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +42 -17
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +89 -0
package/src/editor/shortcuts.js
CHANGED
|
@@ -5,45 +5,48 @@
|
|
|
5
5
|
* shortcuts on the canvas / document.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { getNodeAtPath, parentElementPath, childIndex, canvasWrap } from "../store.js";
|
|
9
|
+
import { activeTab, workspace, closeTab } from "../workspace/workspace.js";
|
|
8
10
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parentElementPath,
|
|
17
|
-
childIndex,
|
|
18
|
-
canvasWrap,
|
|
19
|
-
update,
|
|
20
|
-
} from "../store.js";
|
|
11
|
+
transactDoc,
|
|
12
|
+
mutateInsertNode,
|
|
13
|
+
mutateRemoveNode,
|
|
14
|
+
mutateDuplicateNode,
|
|
15
|
+
undo as tabUndo,
|
|
16
|
+
redo as tabRedo,
|
|
17
|
+
} from "../tabs/transact.js";
|
|
21
18
|
import { isEditing } from "./inline-edit.js";
|
|
22
19
|
import { copyNode, cutNode, pasteNode } from "./context-menu.js";
|
|
23
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {import("../state.js").StudioState} StudioState
|
|
23
|
+
*
|
|
24
|
+
* @typedef {import("../state.js").JxPath} JxPath
|
|
25
|
+
*/
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* Initialise all keyboard (and wheel/pointer) shortcuts.
|
|
26
29
|
*
|
|
27
30
|
* @param {() => {
|
|
28
|
-
* S:
|
|
29
|
-
* setS: (s:
|
|
31
|
+
* S: StudioState;
|
|
32
|
+
* setS: (s: StudioState) => void;
|
|
30
33
|
* canvasMode: string;
|
|
31
34
|
* panX: number;
|
|
32
35
|
* panY: number;
|
|
33
36
|
* setPan: (x: number, y: number) => void;
|
|
34
37
|
* applyTransform: () => void;
|
|
35
38
|
* positionZoomIndicator: () => void;
|
|
36
|
-
* componentInlineEdit:
|
|
39
|
+
* componentInlineEdit: object | null;
|
|
37
40
|
* saveFile: () => void;
|
|
38
41
|
* openProject: () => void;
|
|
39
|
-
* enterEditOnPath: (path:
|
|
42
|
+
* enterEditOnPath: (path: JxPath) => void;
|
|
40
43
|
* }} getContext
|
|
41
44
|
*/
|
|
42
45
|
export function initShortcuts(getContext) {
|
|
43
46
|
// Wheel handler: Ctrl+Scroll = zoom (cursor-centered), plain scroll = pan
|
|
44
47
|
canvasWrap.addEventListener(
|
|
45
48
|
"wheel",
|
|
46
|
-
(/** @type {
|
|
49
|
+
(/** @type {WheelEvent} */ e) => {
|
|
47
50
|
const { S, setS, canvasMode, panX, panY, setPan, applyTransform } = getContext();
|
|
48
51
|
// Edit (content) mode: let the scroll container handle scrolling natively
|
|
49
52
|
if (canvasMode === "edit") return;
|
|
@@ -73,7 +76,7 @@ export function initShortcuts(getContext) {
|
|
|
73
76
|
);
|
|
74
77
|
|
|
75
78
|
// Middle-mouse drag panning
|
|
76
|
-
canvasWrap.addEventListener("pointerdown", (/** @type {
|
|
79
|
+
canvasWrap.addEventListener("pointerdown", (/** @type {PointerEvent} */ e) => {
|
|
77
80
|
const ctx = getContext();
|
|
78
81
|
if (ctx.canvasMode === "edit") return; // no panning in edit mode
|
|
79
82
|
if (e.button !== 1) return; // middle button only
|
|
@@ -81,7 +84,7 @@ export function initShortcuts(getContext) {
|
|
|
81
84
|
canvasWrap.setPointerCapture(e.pointerId);
|
|
82
85
|
let lastX = e.clientX,
|
|
83
86
|
lastY = e.clientY;
|
|
84
|
-
const onMove = (/** @type {
|
|
87
|
+
const onMove = (/** @type {PointerEvent} */ ev) => {
|
|
85
88
|
const { panX, panY, setPan, applyTransform } = getContext();
|
|
86
89
|
setPan(panX + (ev.clientX - lastX), panY + (ev.clientY - lastY));
|
|
87
90
|
lastX = ev.clientX;
|
|
@@ -125,14 +128,19 @@ export function initShortcuts(getContext) {
|
|
|
125
128
|
e.preventDefault();
|
|
126
129
|
saveFile();
|
|
127
130
|
}
|
|
131
|
+
if (mod && e.key === "w") {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
}
|
|
128
134
|
return;
|
|
129
135
|
}
|
|
130
136
|
if (isEditing()) {
|
|
131
|
-
// Let inline editor handle its own keyboard events; only intercept Save
|
|
132
137
|
if (mod && e.key === "s") {
|
|
133
138
|
e.preventDefault();
|
|
134
139
|
saveFile();
|
|
135
140
|
}
|
|
141
|
+
if (mod && e.key === "w") {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
}
|
|
136
144
|
return;
|
|
137
145
|
}
|
|
138
146
|
if (componentInlineEdit) {
|
|
@@ -140,11 +148,25 @@ export function initShortcuts(getContext) {
|
|
|
140
148
|
e.preventDefault();
|
|
141
149
|
saveFile();
|
|
142
150
|
}
|
|
151
|
+
if (mod && e.key === "w") {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
}
|
|
143
154
|
return;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
if (mod) {
|
|
147
158
|
switch (e.key) {
|
|
159
|
+
case "w":
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
if (workspace.activeTabId && workspace.tabOrder.length > 1) {
|
|
162
|
+
const tab = workspace.tabs.get(workspace.activeTabId);
|
|
163
|
+
if (tab?.doc.dirty) {
|
|
164
|
+
const name = tab.documentPath?.split("/").pop() || "Untitled";
|
|
165
|
+
if (!window.confirm(`"${name}" has unsaved changes. Close without saving?`)) break;
|
|
166
|
+
}
|
|
167
|
+
closeTab(workspace.activeTabId);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
148
170
|
case "o":
|
|
149
171
|
e.preventDefault();
|
|
150
172
|
openProject();
|
|
@@ -155,11 +177,15 @@ export function initShortcuts(getContext) {
|
|
|
155
177
|
break;
|
|
156
178
|
case "z":
|
|
157
179
|
e.preventDefault();
|
|
158
|
-
|
|
180
|
+
if (e.shiftKey) tabRedo(activeTab.value);
|
|
181
|
+
else tabUndo(activeTab.value);
|
|
159
182
|
break;
|
|
160
183
|
case "d":
|
|
161
184
|
e.preventDefault();
|
|
162
|
-
if (S.selection)
|
|
185
|
+
if (S.selection) {
|
|
186
|
+
const sel = S.selection;
|
|
187
|
+
transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, sel));
|
|
188
|
+
}
|
|
163
189
|
break;
|
|
164
190
|
case "c":
|
|
165
191
|
e.preventDefault();
|
|
@@ -202,21 +228,23 @@ export function initShortcuts(getContext) {
|
|
|
202
228
|
case "Backspace":
|
|
203
229
|
if (S.selection && S.selection.length >= 2) {
|
|
204
230
|
e.preventDefault();
|
|
205
|
-
|
|
231
|
+
const sel = S.selection;
|
|
232
|
+
transactDoc(activeTab.value, (t) => mutateRemoveNode(t, sel));
|
|
206
233
|
}
|
|
207
234
|
break;
|
|
208
235
|
case "Escape":
|
|
209
|
-
|
|
236
|
+
activeTab.value.session.selection = null;
|
|
210
237
|
break;
|
|
211
238
|
case "Enter":
|
|
212
239
|
if (S.selection && S.selection.length >= 2) {
|
|
213
240
|
e.preventDefault();
|
|
214
|
-
const pp = /** @type {
|
|
241
|
+
const pp = /** @type {JxPath} */ (parentElementPath(S.selection));
|
|
215
242
|
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
216
|
-
let s = insertNode(S, pp, idx + 1, { tagName: "p", textContent: "" });
|
|
217
243
|
const newPath = [...pp, "children", idx + 1];
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
transactDoc(activeTab.value, (t) => {
|
|
245
|
+
mutateInsertNode(t, pp, idx + 1, { tagName: "p", textContent: "" });
|
|
246
|
+
t.session.selection = newPath;
|
|
247
|
+
});
|
|
220
248
|
enterEditOnPath(newPath);
|
|
221
249
|
}
|
|
222
250
|
break;
|
|
@@ -231,7 +259,7 @@ export function initShortcuts(getContext) {
|
|
|
231
259
|
case "ArrowLeft":
|
|
232
260
|
e.preventDefault();
|
|
233
261
|
if (S.selection && S.selection.length >= 2) {
|
|
234
|
-
|
|
262
|
+
activeTab.value.session.selection = parentElementPath(S.selection);
|
|
235
263
|
}
|
|
236
264
|
break;
|
|
237
265
|
case "ArrowRight":
|
|
@@ -239,7 +267,7 @@ export function initShortcuts(getContext) {
|
|
|
239
267
|
if (S.selection) {
|
|
240
268
|
const node = getNodeAtPath(S.document, S.selection);
|
|
241
269
|
if (node?.children?.length > 0) {
|
|
242
|
-
|
|
270
|
+
activeTab.value.session.selection = [...S.selection, "children", 0];
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
273
|
break;
|
|
@@ -249,8 +277,8 @@ export function initShortcuts(getContext) {
|
|
|
249
277
|
// Block ctrl+scroll (browser zoom) on all non-canvas areas
|
|
250
278
|
document.addEventListener(
|
|
251
279
|
"wheel",
|
|
252
|
-
(/** @type {
|
|
253
|
-
if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(e.target)) {
|
|
280
|
+
(/** @type {WheelEvent} */ e) => {
|
|
281
|
+
if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(/** @type {Node} */ (e.target))) {
|
|
254
282
|
e.preventDefault();
|
|
255
283
|
}
|
|
256
284
|
},
|
|
@@ -259,22 +287,24 @@ export function initShortcuts(getContext) {
|
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
/**
|
|
262
|
-
* @param {
|
|
290
|
+
* @param {StudioState} S
|
|
263
291
|
* @param {number} [direction]
|
|
264
292
|
*/
|
|
265
293
|
function navigateSelection(S, direction = -1) {
|
|
266
294
|
if (!S.selection) {
|
|
267
|
-
|
|
295
|
+
activeTab.value.session.selection = [];
|
|
268
296
|
return;
|
|
269
297
|
}
|
|
270
|
-
if (S.selection.length < 2) return;
|
|
298
|
+
if (S.selection.length < 2) return;
|
|
271
299
|
|
|
272
|
-
const parent = getNodeAtPath(S.document, /** @type {
|
|
300
|
+
const parent = getNodeAtPath(S.document, /** @type {JxPath} */ (parentElementPath(S.selection)));
|
|
273
301
|
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
274
302
|
const newIdx = idx + direction;
|
|
275
303
|
if (parent?.children && newIdx >= 0 && newIdx < parent.children.length) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
304
|
+
activeTab.value.session.selection = [
|
|
305
|
+
.../** @type {JxPath} */ (parentElementPath(S.selection)),
|
|
306
|
+
"children",
|
|
307
|
+
newIdx,
|
|
308
|
+
];
|
|
279
309
|
}
|
|
280
310
|
}
|
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/files.js
CHANGED
|
@@ -15,10 +15,12 @@ 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";
|
|
18
20
|
|
|
19
21
|
// ─── File icon map ────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
21
|
-
const fileIconMap = /** @type {Record<string,
|
|
23
|
+
const fileIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
|
|
22
24
|
"sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
|
|
23
25
|
"sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
|
|
24
26
|
"sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
|
|
@@ -29,7 +31,7 @@ const fileIconMap = /** @type {Record<string, any>} */ ({
|
|
|
29
31
|
|
|
30
32
|
// ─── File management ──────────────────────────────────────────────────────────
|
|
31
33
|
|
|
32
|
-
async function loadDirectory(/** @type {
|
|
34
|
+
async function loadDirectory(/** @type {string} */ dirPath) {
|
|
33
35
|
if (!projectState) return;
|
|
34
36
|
try {
|
|
35
37
|
const platform = getPlatform();
|
|
@@ -138,7 +140,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
|
|
|
138
140
|
|
|
139
141
|
// ─── File tree templates ──────────────────────────────────────────────────────
|
|
140
142
|
|
|
141
|
-
function fileTypeIconTpl(/** @type {
|
|
143
|
+
function fileTypeIconTpl(/** @type {string} */ name, /** @type {string} */ type) {
|
|
142
144
|
let tag;
|
|
143
145
|
if (type === "directory") {
|
|
144
146
|
tag = projectState?.expanded?.has(name) ? "sp-icon-folder-open" : "sp-icon-folder";
|
|
@@ -237,11 +239,11 @@ export function renderFilesTemplate({
|
|
|
237
239
|
quiet
|
|
238
240
|
placeholder="Filter files…"
|
|
239
241
|
value=${projectState.searchQuery}
|
|
240
|
-
@input=${(/** @type {
|
|
241
|
-
projectState.searchQuery = e.target.value;
|
|
242
|
+
@input=${(/** @type {Event} */ e) => {
|
|
243
|
+
projectState.searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
|
|
242
244
|
renderLeftPanel();
|
|
243
245
|
}}
|
|
244
|
-
@submit=${(/** @type {
|
|
246
|
+
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
245
247
|
></sp-search>
|
|
246
248
|
</div>
|
|
247
249
|
<div class="file-tree" role="tree" aria-label="Project files">
|
|
@@ -250,10 +252,10 @@ export function renderFilesTemplate({
|
|
|
250
252
|
`;
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
/** @returns {
|
|
255
|
+
/** @returns {import("lit-html").TemplateResult | import("lit-html").TemplateResult[]} */
|
|
254
256
|
function renderTreeLevelTemplate(
|
|
255
|
-
/** @type {
|
|
256
|
-
/** @type {
|
|
257
|
+
/** @type {string} */ dirPath,
|
|
258
|
+
/** @type {number} */ depth,
|
|
257
259
|
/** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
|
|
258
260
|
) {
|
|
259
261
|
const entries = projectState.dirs.get(dirPath);
|
|
@@ -293,7 +295,7 @@ function renderTreeLevelTemplate(
|
|
|
293
295
|
data-path=${entry.path}
|
|
294
296
|
data-type=${entry.type}
|
|
295
297
|
aria-expanded=${isDir ? String(isExpanded) : nothing}
|
|
296
|
-
@click=${async (/** @type {
|
|
298
|
+
@click=${async (/** @type {MouseEvent} */ e) => {
|
|
297
299
|
e.stopPropagation();
|
|
298
300
|
if (isDir) {
|
|
299
301
|
if (isExpanded) projectState.expanded.delete(entry.path);
|
|
@@ -306,7 +308,7 @@ function renderTreeLevelTemplate(
|
|
|
306
308
|
ctx.openFileFn(entry.path);
|
|
307
309
|
}
|
|
308
310
|
}}
|
|
309
|
-
@contextmenu=${(/** @type {
|
|
311
|
+
@contextmenu=${(/** @type {MouseEvent} */ e) => {
|
|
310
312
|
e.preventDefault();
|
|
311
313
|
e.stopPropagation();
|
|
312
314
|
showFileContextMenu(e, entry, ctx);
|
|
@@ -325,10 +327,10 @@ function renderTreeLevelTemplate(
|
|
|
325
327
|
});
|
|
326
328
|
}
|
|
327
329
|
|
|
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");
|
|
330
|
+
export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
|
|
331
|
+
tree.addEventListener("keydown", (/** @type {KeyboardEvent} */ e) => {
|
|
332
|
+
const items = /** @type {HTMLElement[]} */ ([...tree.querySelectorAll(".file-tree-item")]);
|
|
333
|
+
const focused = /** @type {HTMLElement | null} */ (tree.querySelector(".file-tree-item:focus"));
|
|
332
334
|
if (!focused || items.length === 0) return;
|
|
333
335
|
|
|
334
336
|
const idx = items.indexOf(focused);
|
|
@@ -343,12 +345,15 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
|
|
|
343
345
|
break;
|
|
344
346
|
case "ArrowRight":
|
|
345
347
|
if (focused.dataset.type === "directory") {
|
|
346
|
-
const path = focused.dataset.path;
|
|
348
|
+
const path = /** @type {string} */ (focused.dataset.path);
|
|
347
349
|
if (!projectState.expanded.has(path)) {
|
|
348
350
|
projectState.expanded.add(path);
|
|
349
351
|
loadDirectory(path).then(() => {
|
|
350
352
|
const panel = tree.closest(".panel-body");
|
|
351
|
-
if (panel)
|
|
353
|
+
if (panel)
|
|
354
|
+
/** @type {HTMLElement | null} */ (
|
|
355
|
+
panel.querySelector(".file-tree-item:focus")
|
|
356
|
+
)?.click();
|
|
352
357
|
});
|
|
353
358
|
}
|
|
354
359
|
}
|
|
@@ -378,12 +383,12 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
|
|
|
378
383
|
|
|
379
384
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
380
385
|
|
|
381
|
-
/** @type {
|
|
386
|
+
/** @type {HTMLElement | null} */
|
|
382
387
|
let fileContextPopover = null;
|
|
383
388
|
|
|
384
389
|
function showFileContextMenu(
|
|
385
|
-
/** @type {
|
|
386
|
-
/** @type {
|
|
390
|
+
/** @type {MouseEvent} */ e,
|
|
391
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
387
392
|
/** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
|
|
388
393
|
) {
|
|
389
394
|
if (fileContextPopover) {
|
|
@@ -442,8 +447,8 @@ function showFileContextMenu(
|
|
|
442
447
|
litRender(tpl, fileContextPopover);
|
|
443
448
|
document.body.appendChild(fileContextPopover);
|
|
444
449
|
|
|
445
|
-
const closeHandler = (/** @type {
|
|
446
|
-
if (!fileContextPopover?.contains(ev.target)) {
|
|
450
|
+
const closeHandler = (/** @type {MouseEvent} */ ev) => {
|
|
451
|
+
if (!fileContextPopover?.contains(/** @type {Node} */ (ev.target))) {
|
|
447
452
|
closeFileContextMenu();
|
|
448
453
|
document.removeEventListener("mousedown", closeHandler, true);
|
|
449
454
|
}
|
|
@@ -478,7 +483,10 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
|
|
|
478
483
|
}
|
|
479
484
|
}
|
|
480
485
|
|
|
481
|
-
async function renameFile(
|
|
486
|
+
async function renameFile(
|
|
487
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
488
|
+
/** @type {() => void} */ renderLeftPanel,
|
|
489
|
+
) {
|
|
482
490
|
const newName = prompt("New name:", entry.name);
|
|
483
491
|
if (!newName || newName === entry.name) return;
|
|
484
492
|
const entryPath = entry.path.replaceAll("\\", "/");
|
|
@@ -500,7 +508,10 @@ async function renameFile(/** @type {any} */ entry, /** @type {() => void} */ re
|
|
|
500
508
|
}
|
|
501
509
|
}
|
|
502
510
|
|
|
503
|
-
async function deleteFile(
|
|
511
|
+
async function deleteFile(
|
|
512
|
+
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
513
|
+
/** @type {() => void} */ renderLeftPanel,
|
|
514
|
+
) {
|
|
504
515
|
if (!confirm(`Delete "${entry.name}"?`)) return;
|
|
505
516
|
try {
|
|
506
517
|
const platform = getPlatform();
|
|
@@ -583,3 +594,40 @@ export async function openFileFromTree(ctx, path) {
|
|
|
583
594
|
statusMessage(`Error: ${e.message}`);
|
|
584
595
|
}
|
|
585
596
|
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Open a file from the tree into a tab. Activates existing tab if already open.
|
|
600
|
+
*
|
|
601
|
+
* @param {string} path
|
|
602
|
+
*/
|
|
603
|
+
export async function openFileInTab(path) {
|
|
604
|
+
for (const [id, tab] of workspace.tabs.entries()) {
|
|
605
|
+
if (tab.documentPath === path) {
|
|
606
|
+
activateTab(id);
|
|
607
|
+
projectState.selectedPath = path;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const platform = getPlatform();
|
|
613
|
+
try {
|
|
614
|
+
const content = await platform.readFile(path);
|
|
615
|
+
if (!content) return;
|
|
616
|
+
|
|
617
|
+
let document, frontmatter;
|
|
618
|
+
if (path.endsWith(".md")) {
|
|
619
|
+
const state = await loadMarkdown(content, null);
|
|
620
|
+
document = state.document;
|
|
621
|
+
frontmatter = state.content?.frontmatter;
|
|
622
|
+
} else {
|
|
623
|
+
document = JSON.parse(content);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const id = path;
|
|
627
|
+
openTab({ id, documentPath: path, document, frontmatter });
|
|
628
|
+
projectState.selectedPath = path;
|
|
629
|
+
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
630
|
+
} catch (/** @type {any} */ e) {
|
|
631
|
+
statusMessage(`Error: ${e.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
@@ -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 } from "../store.js";
|
|
5
|
+
import { effect, effectScope } from "../reactivity.js";
|
|
6
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
7
|
+
|
|
8
|
+
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
9
|
+
let _scope = null;
|
|
10
|
+
|
|
11
|
+
export function mount() {
|
|
12
|
+
_scope = effectScope();
|
|
13
|
+
_scope.run(() => {
|
|
14
|
+
effect(() => {
|
|
15
|
+
const tab = activeTab.value;
|
|
16
|
+
if (!tab) return;
|
|
17
|
+
void tab.session.ui.leftTab;
|
|
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 = tab.session.ui.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,11 @@ 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
|
-
update({ ...current, ui: { ...current.ui, leftTab: e.target.selected } });
|
|
81
|
-
renderOnly("activityBar", "leftPanel");
|
|
103
|
+
activeTab.value.session.ui.leftTab = e.target.selected;
|
|
82
104
|
}}
|
|
83
105
|
>
|
|
84
106
|
${tabs.map(
|