@jxsuite/studio 0.19.0 → 0.21.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.css +98 -98
- package/dist/studio.js +13476 -12330
- package/dist/studio.js.map +152 -148
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/tab-strip.js +5 -2
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +8 -8
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/layers.js +31 -1
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +29 -7
package/src/files/files.js
CHANGED
|
@@ -7,20 +7,28 @@
|
|
|
7
7
|
|
|
8
8
|
import { html, nothing } from "lit-html";
|
|
9
9
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
10
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
10
11
|
import { unified } from "unified";
|
|
11
12
|
import remarkStringify from "remark-stringify";
|
|
12
|
-
import { renderPopover } from "../ui/layers.js";
|
|
13
|
+
import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers.js";
|
|
13
14
|
import remarkDirective from "remark-directive";
|
|
14
15
|
import { stringify as stringifyYaml } from "yaml";
|
|
15
16
|
import { jxToMd } from "../markdown/md-convert.js";
|
|
16
|
-
import { createState, projectState, setProjectState } from "../store.js";
|
|
17
|
+
import { createState, projectState, setProjectState, requireProjectState } from "../store.js";
|
|
17
18
|
import { getPlatform } from "../platform.js";
|
|
18
19
|
import { statusMessage } from "../panels/statusbar.js";
|
|
19
20
|
import { loadComponentRegistry } from "./components.js";
|
|
21
|
+
import {
|
|
22
|
+
draggable,
|
|
23
|
+
dropTargetForElements,
|
|
24
|
+
monitorForElements,
|
|
25
|
+
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
26
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
20
27
|
import {
|
|
21
28
|
workspace,
|
|
22
29
|
openTab,
|
|
23
30
|
activateTab,
|
|
31
|
+
renameTab,
|
|
24
32
|
replaceAllTabs,
|
|
25
33
|
activeTab,
|
|
26
34
|
} from "../workspace/workspace.js";
|
|
@@ -65,7 +73,7 @@ export async function loadProject() {
|
|
|
65
73
|
name: info.isSiteProject ? info.projectConfig?.name || meta.name : meta.name,
|
|
66
74
|
projectRoot: ".",
|
|
67
75
|
isSiteProject: info.isSiteProject,
|
|
68
|
-
projectConfig: info.isSiteProject ? info.projectConfig : null,
|
|
76
|
+
projectConfig: (info.isSiteProject ? info.projectConfig : null) || null,
|
|
69
77
|
projectDirs: info.directories || [],
|
|
70
78
|
dirs: new Map(),
|
|
71
79
|
expanded: new Set(),
|
|
@@ -105,7 +113,7 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
|
|
|
105
113
|
replaceAllTabs({ id: "initial", document: { tagName: "div", children: [] } });
|
|
106
114
|
|
|
107
115
|
setProjectState({
|
|
108
|
-
|
|
116
|
+
.../** @type {ProjectState} */ (projectState),
|
|
109
117
|
projectRoot: handle.root,
|
|
110
118
|
isSiteProject: true,
|
|
111
119
|
projectConfig: config,
|
|
@@ -129,26 +137,26 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
|
|
|
129
137
|
"public",
|
|
130
138
|
"styles",
|
|
131
139
|
];
|
|
132
|
-
const entries =
|
|
140
|
+
const entries = requireProjectState().dirs.get(".") || [];
|
|
133
141
|
const foundDirs = [];
|
|
134
142
|
for (const e of entries) {
|
|
135
143
|
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
136
144
|
foundDirs.push(e.name);
|
|
137
|
-
|
|
145
|
+
requireProjectState().expanded.add(e.path || e.name);
|
|
138
146
|
await loadDirectory(e.path || e.name);
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
|
-
|
|
149
|
+
requireProjectState().projectDirs = foundDirs;
|
|
142
150
|
|
|
143
151
|
view.leftTab = "files";
|
|
144
|
-
addRecentProject(
|
|
152
|
+
addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
|
|
145
153
|
renderActivityBar();
|
|
146
154
|
renderLeftPanel();
|
|
147
|
-
statusMessage(`Opened project: ${
|
|
155
|
+
statusMessage(`Opened project: ${requireProjectState().name}`);
|
|
148
156
|
|
|
149
157
|
await openHomePage();
|
|
150
|
-
} catch (/** @type {
|
|
151
|
-
statusMessage(`Error: ${e.message}`);
|
|
158
|
+
} catch (/** @type {unknown} */ e) {
|
|
159
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
|
|
@@ -251,9 +259,9 @@ export function renderFilesTemplate({
|
|
|
251
259
|
size="xs"
|
|
252
260
|
label="Refresh"
|
|
253
261
|
@click=${async () => {
|
|
254
|
-
|
|
262
|
+
requireProjectState().dirs.clear();
|
|
255
263
|
await loadDirectory(".");
|
|
256
|
-
for (const dir of
|
|
264
|
+
for (const dir of requireProjectState().expanded) await loadDirectory(dir);
|
|
257
265
|
renderLeftPanel();
|
|
258
266
|
}}
|
|
259
267
|
>
|
|
@@ -264,9 +272,9 @@ export function renderFilesTemplate({
|
|
|
264
272
|
size="s"
|
|
265
273
|
quiet
|
|
266
274
|
placeholder="Filter files…"
|
|
267
|
-
value=${
|
|
275
|
+
value=${requireProjectState().searchQuery}
|
|
268
276
|
@input=${(/** @type {Event} */ e) => {
|
|
269
|
-
|
|
277
|
+
requireProjectState().searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
|
|
270
278
|
renderLeftPanel();
|
|
271
279
|
}}
|
|
272
280
|
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
@@ -284,7 +292,7 @@ function renderTreeLevelTemplate(
|
|
|
284
292
|
/** @type {number} */ depth,
|
|
285
293
|
/** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
|
|
286
294
|
) {
|
|
287
|
-
const entries =
|
|
295
|
+
const entries = requireProjectState().dirs.get(dirPath);
|
|
288
296
|
if (!entries) {
|
|
289
297
|
loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
|
|
290
298
|
return html`<div
|
|
@@ -301,15 +309,15 @@ function renderTreeLevelTemplate(
|
|
|
301
309
|
return a.name.localeCompare(b.name);
|
|
302
310
|
});
|
|
303
311
|
|
|
304
|
-
const query =
|
|
312
|
+
const query = requireProjectState().searchQuery.toLowerCase();
|
|
305
313
|
const filtered = query
|
|
306
314
|
? sorted.filter((e) => e.type === "directory" || e.name.toLowerCase().includes(query))
|
|
307
315
|
: sorted;
|
|
308
316
|
|
|
309
317
|
return filtered.map((entry) => {
|
|
310
318
|
const isDir = entry.type === "directory";
|
|
311
|
-
const isExpanded =
|
|
312
|
-
const isSelected =
|
|
319
|
+
const isExpanded = requireProjectState().expanded.has(entry.path);
|
|
320
|
+
const isSelected = requireProjectState().selectedPath === entry.path;
|
|
313
321
|
|
|
314
322
|
return html`
|
|
315
323
|
<div
|
|
@@ -324,10 +332,10 @@ function renderTreeLevelTemplate(
|
|
|
324
332
|
@click=${async (/** @type {MouseEvent} */ e) => {
|
|
325
333
|
e.stopPropagation();
|
|
326
334
|
if (isDir) {
|
|
327
|
-
if (isExpanded)
|
|
335
|
+
if (isExpanded) requireProjectState().expanded.delete(entry.path);
|
|
328
336
|
else {
|
|
329
|
-
|
|
330
|
-
if (!
|
|
337
|
+
requireProjectState().expanded.add(entry.path);
|
|
338
|
+
if (!requireProjectState().dirs.has(entry.path)) await loadDirectory(entry.path);
|
|
331
339
|
}
|
|
332
340
|
ctx.renderLeftPanel();
|
|
333
341
|
} else {
|
|
@@ -372,8 +380,8 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
|
|
|
372
380
|
case "ArrowRight":
|
|
373
381
|
if (focused.dataset.type === "directory") {
|
|
374
382
|
const path = /** @type {string} */ (focused.dataset.path);
|
|
375
|
-
if (!
|
|
376
|
-
|
|
383
|
+
if (!requireProjectState().expanded.has(path)) {
|
|
384
|
+
requireProjectState().expanded.add(path);
|
|
377
385
|
loadDirectory(path).then(() => {
|
|
378
386
|
const panel = tree.closest(".panel-body");
|
|
379
387
|
if (panel)
|
|
@@ -386,9 +394,9 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
|
|
|
386
394
|
break;
|
|
387
395
|
case "ArrowLeft":
|
|
388
396
|
if (focused.dataset.type === "directory") {
|
|
389
|
-
const path = focused.dataset.path;
|
|
390
|
-
if (
|
|
391
|
-
|
|
397
|
+
const path = /** @type {string} */ (focused.dataset.path);
|
|
398
|
+
if (requireProjectState().expanded.has(path)) {
|
|
399
|
+
requireProjectState().expanded.delete(path);
|
|
392
400
|
// renderLeftPanel will be called by the caller who sets up keyboard
|
|
393
401
|
}
|
|
394
402
|
}
|
|
@@ -407,6 +415,170 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
|
|
|
407
415
|
if (first) first.setAttribute("tabindex", "0");
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
/** @type {(() => void)[]} */
|
|
419
|
+
let _fileTreeDndCleanups = [];
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Register drag-and-drop on file tree items. Called after each file tree render.
|
|
423
|
+
*
|
|
424
|
+
* @param {{ renderLeftPanel: () => void }} ctx
|
|
425
|
+
*/
|
|
426
|
+
export function registerFileTreeDnD({ renderLeftPanel }) {
|
|
427
|
+
// Clean up previous registrations
|
|
428
|
+
for (const fn of _fileTreeDndCleanups) fn();
|
|
429
|
+
_fileTreeDndCleanups = [];
|
|
430
|
+
|
|
431
|
+
requestAnimationFrame(() => {
|
|
432
|
+
const tree = /** @type {HTMLElement | null} */ (document.querySelector(".file-tree"));
|
|
433
|
+
if (!tree) return;
|
|
434
|
+
|
|
435
|
+
const items = /** @type {NodeListOf<HTMLElement>} */ (tree.querySelectorAll(".file-tree-item"));
|
|
436
|
+
|
|
437
|
+
items.forEach((row) => {
|
|
438
|
+
const path = row.dataset.path;
|
|
439
|
+
const type = row.dataset.type;
|
|
440
|
+
if (!path) return;
|
|
441
|
+
|
|
442
|
+
const cleanups = [
|
|
443
|
+
draggable({
|
|
444
|
+
element: row,
|
|
445
|
+
getInitialData() {
|
|
446
|
+
return { type: "file-tree", path, entryType: type };
|
|
447
|
+
},
|
|
448
|
+
onDragStart() {
|
|
449
|
+
row.classList.add("dragging");
|
|
450
|
+
},
|
|
451
|
+
onDrop() {
|
|
452
|
+
row.classList.remove("dragging");
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
if (type === "directory") {
|
|
458
|
+
cleanups.push(
|
|
459
|
+
dropTargetForElements({
|
|
460
|
+
element: row,
|
|
461
|
+
canDrop({ source }) {
|
|
462
|
+
if (source.data.type !== "file-tree") return false;
|
|
463
|
+
const srcPath = /** @type {string} */ (source.data.path);
|
|
464
|
+
if (srcPath === path) return false;
|
|
465
|
+
if (srcPath.startsWith(path + "/")) return false;
|
|
466
|
+
const srcParent = parentDir(srcPath);
|
|
467
|
+
if (srcParent === path) return false;
|
|
468
|
+
return true;
|
|
469
|
+
},
|
|
470
|
+
onDragEnter() {
|
|
471
|
+
row.classList.add("drag-over");
|
|
472
|
+
},
|
|
473
|
+
onDrag() {
|
|
474
|
+
if (!row.classList.contains("drag-over")) row.classList.add("drag-over");
|
|
475
|
+
},
|
|
476
|
+
onDragLeave() {
|
|
477
|
+
row.classList.remove("drag-over");
|
|
478
|
+
},
|
|
479
|
+
onDrop() {
|
|
480
|
+
row.classList.remove("drag-over");
|
|
481
|
+
},
|
|
482
|
+
getData() {
|
|
483
|
+
return { type: "file-tree-target", targetDir: path };
|
|
484
|
+
},
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_fileTreeDndCleanups.push(combine(...cleanups));
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Root-level drop target (move to project root)
|
|
493
|
+
const rootCleanup = dropTargetForElements({
|
|
494
|
+
element: tree,
|
|
495
|
+
canDrop({ source }) {
|
|
496
|
+
if (source.data.type !== "file-tree") return false;
|
|
497
|
+
const srcPath = /** @type {string} */ (source.data.path);
|
|
498
|
+
return parentDir(srcPath) !== ".";
|
|
499
|
+
},
|
|
500
|
+
onDragEnter() {
|
|
501
|
+
tree.classList.add("drag-over-root");
|
|
502
|
+
},
|
|
503
|
+
onDragLeave() {
|
|
504
|
+
tree.classList.remove("drag-over-root");
|
|
505
|
+
},
|
|
506
|
+
onDrop() {
|
|
507
|
+
tree.classList.remove("drag-over-root");
|
|
508
|
+
},
|
|
509
|
+
getData() {
|
|
510
|
+
return { type: "file-tree-target", targetDir: "." };
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
_fileTreeDndCleanups.push(rootCleanup);
|
|
514
|
+
|
|
515
|
+
// Monitor for drop events
|
|
516
|
+
const monitorCleanup = monitorForElements({
|
|
517
|
+
onDrop({ source, location }) {
|
|
518
|
+
const target = location.current.dropTargets[0];
|
|
519
|
+
if (!target) return;
|
|
520
|
+
if (source.data.type !== "file-tree") return;
|
|
521
|
+
if (target.data.type !== "file-tree-target") return;
|
|
522
|
+
|
|
523
|
+
const srcPath = /** @type {string} */ (source.data.path);
|
|
524
|
+
const targetDirPath = /** @type {string} */ (target.data.targetDir);
|
|
525
|
+
const fileName = srcPath.split("/").pop();
|
|
526
|
+
const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
|
|
527
|
+
|
|
528
|
+
if (newPath === srcPath) return;
|
|
529
|
+
|
|
530
|
+
moveFileEntry(srcPath, /** @type {string} */ (newPath), renderLeftPanel);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
_fileTreeDndCleanups.push(monitorCleanup);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Move a file/directory and update all affected state.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} oldPath
|
|
541
|
+
* @param {string} newPath
|
|
542
|
+
* @param {() => void} renderLeftPanel
|
|
543
|
+
*/
|
|
544
|
+
async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
|
|
545
|
+
const platform = getPlatform();
|
|
546
|
+
try {
|
|
547
|
+
await platform.renameFile(oldPath, newPath);
|
|
548
|
+
|
|
549
|
+
// Update open tabs referencing the moved path
|
|
550
|
+
for (const [id] of workspace.tabs.entries()) {
|
|
551
|
+
if (id === oldPath) {
|
|
552
|
+
renameTab(oldPath, newPath, newPath);
|
|
553
|
+
} else if (id.startsWith(oldPath + "/")) {
|
|
554
|
+
const newTabPath = newPath + id.slice(oldPath.length);
|
|
555
|
+
renameTab(id, newTabPath, newTabPath);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Refresh affected directories
|
|
560
|
+
const oldParent = parentDir(oldPath);
|
|
561
|
+
const newParent = parentDir(newPath);
|
|
562
|
+
await loadDirectory(oldParent);
|
|
563
|
+
if (newParent !== oldParent) await loadDirectory(newParent);
|
|
564
|
+
|
|
565
|
+
// Auto-expand target directory
|
|
566
|
+
if (newParent !== ".") requireProjectState().expanded.add(newParent);
|
|
567
|
+
|
|
568
|
+
renderLeftPanel();
|
|
569
|
+
statusMessage(`Moved to ${newPath}`);
|
|
570
|
+
} catch (/** @type {unknown} */ e) {
|
|
571
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** @param {string} path @returns {string} */
|
|
576
|
+
function parentDir(path) {
|
|
577
|
+
const normalized = path.replaceAll("\\", "/");
|
|
578
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
579
|
+
return lastSlash === -1 ? "." : normalized.substring(0, lastSlash);
|
|
580
|
+
}
|
|
581
|
+
|
|
410
582
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
411
583
|
|
|
412
584
|
/** @type {ReturnType<typeof renderPopover> | null} */
|
|
@@ -452,7 +624,22 @@ function showFileContextMenu(
|
|
|
452
624
|
y = e.clientY;
|
|
453
625
|
|
|
454
626
|
_fileCtxHandle = renderPopover(
|
|
455
|
-
html`<sp-popover
|
|
627
|
+
html`<sp-popover
|
|
628
|
+
open
|
|
629
|
+
style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
|
|
630
|
+
${ref((el) => {
|
|
631
|
+
if (!el) return;
|
|
632
|
+
requestAnimationFrame(() => {
|
|
633
|
+
const popover = /** @type {HTMLElement} */ (el);
|
|
634
|
+
const menuRect = popover.getBoundingClientRect();
|
|
635
|
+
if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
|
|
636
|
+
if (y + menuRect.height > window.innerHeight)
|
|
637
|
+
y = window.innerHeight - menuRect.height - 4;
|
|
638
|
+
popover.style.left = `${x}px`;
|
|
639
|
+
popover.style.top = `${y}px`;
|
|
640
|
+
});
|
|
641
|
+
})}
|
|
642
|
+
>
|
|
456
643
|
<sp-menu>
|
|
457
644
|
${items.map((item) =>
|
|
458
645
|
item.label === "\u2014"
|
|
@@ -475,18 +662,6 @@ function showFileContextMenu(
|
|
|
475
662
|
},
|
|
476
663
|
},
|
|
477
664
|
);
|
|
478
|
-
|
|
479
|
-
requestAnimationFrame(() => {
|
|
480
|
-
const popover = /** @type {HTMLElement | null} */ (
|
|
481
|
-
_fileCtxHandle?.host.querySelector("sp-popover")
|
|
482
|
-
);
|
|
483
|
-
if (!popover) return;
|
|
484
|
-
const menuRect = popover.getBoundingClientRect();
|
|
485
|
-
if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
|
|
486
|
-
if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
|
|
487
|
-
popover.style.left = `${x}px`;
|
|
488
|
-
popover.style.top = `${y}px`;
|
|
489
|
-
});
|
|
490
665
|
}
|
|
491
666
|
|
|
492
667
|
// ─── File CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -497,23 +672,69 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
|
|
|
497
672
|
const path = dirPath === "." ? name : `${dirPath}/${name}`;
|
|
498
673
|
const content = name.endsWith(".md")
|
|
499
674
|
? "---\ntitle: Untitled\n---\n\n"
|
|
500
|
-
: JSON.stringify({ tagName: "div", children: [] }, null, 2);
|
|
675
|
+
: JSON.stringify({ tagName: "div", children: [{ tagName: "p", children: [] }] }, null, 2);
|
|
501
676
|
try {
|
|
502
677
|
const platform = getPlatform();
|
|
503
678
|
await platform.writeFile(path, content);
|
|
504
679
|
await loadDirectory(dirPath);
|
|
505
680
|
renderLeftPanel();
|
|
506
681
|
statusMessage(`Created ${path}`);
|
|
507
|
-
} catch (/** @type {
|
|
508
|
-
statusMessage(`Error: ${e.message}`);
|
|
682
|
+
} catch (/** @type {unknown} */ e) {
|
|
683
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
509
684
|
}
|
|
510
685
|
}
|
|
511
686
|
|
|
687
|
+
/** @param {string} currentName @returns {Promise<string | null>} */
|
|
688
|
+
function showRenameFileDialog(currentName) {
|
|
689
|
+
let value = currentName;
|
|
690
|
+
return showDialog((done) => {
|
|
691
|
+
function confirm() {
|
|
692
|
+
const trimmed = value.trim();
|
|
693
|
+
if (!trimmed) return;
|
|
694
|
+
done(trimmed);
|
|
695
|
+
}
|
|
696
|
+
const tpl = html`
|
|
697
|
+
<sp-dialog-wrapper
|
|
698
|
+
open
|
|
699
|
+
underlay
|
|
700
|
+
headline="Rename"
|
|
701
|
+
confirm-label="Rename"
|
|
702
|
+
cancel-label="Cancel"
|
|
703
|
+
size="s"
|
|
704
|
+
@confirm=${confirm}
|
|
705
|
+
@cancel=${() => done(null)}
|
|
706
|
+
@close=${() => done(null)}
|
|
707
|
+
>
|
|
708
|
+
<sp-textfield
|
|
709
|
+
style="width:100%"
|
|
710
|
+
value=${currentName}
|
|
711
|
+
@input=${(/** @type {Event} */ e) => {
|
|
712
|
+
value = /** @type {HTMLInputElement} */ (e.target).value || "";
|
|
713
|
+
}}
|
|
714
|
+
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
715
|
+
if (e.key === "Enter") confirm();
|
|
716
|
+
}}
|
|
717
|
+
></sp-textfield>
|
|
718
|
+
</sp-dialog-wrapper>
|
|
719
|
+
`;
|
|
720
|
+
requestAnimationFrame(() => {
|
|
721
|
+
const layer = document.getElementById("layer-dialog");
|
|
722
|
+
const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
|
|
723
|
+
if (tf) {
|
|
724
|
+
tf.focus();
|
|
725
|
+
const input = tf.shadowRoot?.querySelector("input");
|
|
726
|
+
if (input) input.select();
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
return tpl;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
512
733
|
async function renameFile(
|
|
513
734
|
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
514
735
|
/** @type {() => void} */ renderLeftPanel,
|
|
515
736
|
) {
|
|
516
|
-
const newName =
|
|
737
|
+
const newName = await showRenameFileDialog(entry.name);
|
|
517
738
|
if (!newName || newName === entry.name) return;
|
|
518
739
|
const entryPath = entry.path.replaceAll("\\", "/");
|
|
519
740
|
const parentDir = entryPath.includes("/")
|
|
@@ -524,13 +745,16 @@ async function renameFile(
|
|
|
524
745
|
const platform = getPlatform();
|
|
525
746
|
await platform.renameFile(entry.path, newPath);
|
|
526
747
|
await loadDirectory(parentDir);
|
|
527
|
-
if (
|
|
528
|
-
|
|
748
|
+
if (requireProjectState().selectedPath === entry.path) {
|
|
749
|
+
requireProjectState().selectedPath = newPath;
|
|
750
|
+
}
|
|
751
|
+
if (workspace.tabs.has(entry.path)) {
|
|
752
|
+
renameTab(entry.path, newPath, newPath);
|
|
529
753
|
}
|
|
530
754
|
renderLeftPanel();
|
|
531
755
|
statusMessage(`Renamed to ${newName}`);
|
|
532
|
-
} catch (/** @type {
|
|
533
|
-
statusMessage(`Error: ${e.message}`);
|
|
756
|
+
} catch (/** @type {unknown} */ e) {
|
|
757
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
534
758
|
}
|
|
535
759
|
}
|
|
536
760
|
|
|
@@ -538,20 +762,24 @@ async function deleteFile(
|
|
|
538
762
|
/** @type {{ name: string; path: string; type: string }} */ entry,
|
|
539
763
|
/** @type {() => void} */ renderLeftPanel,
|
|
540
764
|
) {
|
|
541
|
-
|
|
765
|
+
const confirmed = await showConfirmDialog("Delete File", `Delete "${entry.name}"?`, {
|
|
766
|
+
confirmLabel: "Delete",
|
|
767
|
+
destructive: true,
|
|
768
|
+
});
|
|
769
|
+
if (!confirmed) return;
|
|
542
770
|
try {
|
|
543
771
|
const platform = getPlatform();
|
|
544
772
|
await platform.deleteFile(entry.path);
|
|
545
773
|
const delPath = entry.path.replaceAll("\\", "/");
|
|
546
774
|
const parentDir = delPath.includes("/") ? delPath.substring(0, delPath.lastIndexOf("/")) : ".";
|
|
547
775
|
await loadDirectory(parentDir);
|
|
548
|
-
if (
|
|
549
|
-
|
|
776
|
+
if (requireProjectState().selectedPath === entry.path) {
|
|
777
|
+
requireProjectState().selectedPath = null;
|
|
550
778
|
}
|
|
551
779
|
renderLeftPanel();
|
|
552
780
|
statusMessage(`Deleted ${entry.name}`);
|
|
553
|
-
} catch (/** @type {
|
|
554
|
-
statusMessage(`Error: ${e.message}`);
|
|
781
|
+
} catch (/** @type {unknown} */ e) {
|
|
782
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
555
783
|
}
|
|
556
784
|
}
|
|
557
785
|
|
|
@@ -561,10 +789,10 @@ async function deleteFile(
|
|
|
561
789
|
* Open a file from the file tree — auto-saves current dirty doc, then loads the new one.
|
|
562
790
|
*
|
|
563
791
|
* @param {{
|
|
564
|
-
* S:
|
|
565
|
-
* commit: (s:
|
|
792
|
+
* S: import("../state.js").StudioState;
|
|
793
|
+
* commit: (s: import("../state.js").StudioState) => void;
|
|
566
794
|
* render: () => void;
|
|
567
|
-
* loadMarkdown: (source: string, handle:
|
|
795
|
+
* loadMarkdown: (source: string, handle: unknown) => void;
|
|
568
796
|
* }} ctx
|
|
569
797
|
* @param {string} path
|
|
570
798
|
*/
|
|
@@ -576,11 +804,11 @@ export async function openFileFromTree(ctx, path) {
|
|
|
576
804
|
const isContent = ctx.S.mode === "content";
|
|
577
805
|
let output;
|
|
578
806
|
if (isContent) {
|
|
579
|
-
const mdast = jxToMd(ctx.S.document);
|
|
807
|
+
const mdast = jxToMd(/** @type {JxElement} */ (ctx.S.document));
|
|
580
808
|
const md = unified()
|
|
581
809
|
.use(remarkDirective)
|
|
582
810
|
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
583
|
-
.stringify(mdast);
|
|
811
|
+
.stringify(/** @type {import("mdast").Root} */ (/** @type {unknown} */ (mdast)));
|
|
584
812
|
const fm = ctx.S.content?.frontmatter;
|
|
585
813
|
const hasFrontmatter = fm && Object.keys(fm).length > 0;
|
|
586
814
|
output = hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
|
|
@@ -588,8 +816,8 @@ export async function openFileFromTree(ctx, path) {
|
|
|
588
816
|
output = JSON.stringify(ctx.S.document, null, 2);
|
|
589
817
|
}
|
|
590
818
|
await platform.writeFile(ctx.S.documentPath, output);
|
|
591
|
-
} catch (/** @type {
|
|
592
|
-
statusMessage(`Save error: ${e.message}`);
|
|
819
|
+
} catch (/** @type {unknown} */ e) {
|
|
820
|
+
statusMessage(`Save error: ${/** @type {Error} */ (e).message}`);
|
|
593
821
|
}
|
|
594
822
|
}
|
|
595
823
|
|
|
@@ -612,12 +840,12 @@ export async function openFileFromTree(ctx, path) {
|
|
|
612
840
|
}
|
|
613
841
|
|
|
614
842
|
// Update tree selection
|
|
615
|
-
|
|
843
|
+
requireProjectState().selectedPath = path;
|
|
616
844
|
|
|
617
845
|
ctx.render();
|
|
618
846
|
statusMessage(`Opened ${path}`);
|
|
619
|
-
} catch (/** @type {
|
|
620
|
-
statusMessage(`Error: ${e.message}`);
|
|
847
|
+
} catch (/** @type {unknown} */ e) {
|
|
848
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
621
849
|
}
|
|
622
850
|
}
|
|
623
851
|
|
|
@@ -630,7 +858,7 @@ export async function openFileInTab(path) {
|
|
|
630
858
|
for (const [id, tab] of workspace.tabs.entries()) {
|
|
631
859
|
if (tab.documentPath === path) {
|
|
632
860
|
activateTab(id);
|
|
633
|
-
|
|
861
|
+
requireProjectState().selectedPath = path;
|
|
634
862
|
return;
|
|
635
863
|
}
|
|
636
864
|
}
|
|
@@ -657,9 +885,8 @@ export async function openFileInTab(path) {
|
|
|
657
885
|
frontmatter,
|
|
658
886
|
sourceFormat: path.endsWith(".md") ? "md" : null,
|
|
659
887
|
});
|
|
660
|
-
|
|
888
|
+
requireProjectState().selectedPath = path;
|
|
661
889
|
trackRecentFile({ path, name: path.split("/").pop() || path });
|
|
662
|
-
view.leftTab = "layers";
|
|
663
890
|
|
|
664
891
|
if (path === "project.json") {
|
|
665
892
|
const tab = activeTab.value;
|
|
@@ -667,7 +894,34 @@ export async function openFileInTab(path) {
|
|
|
667
894
|
}
|
|
668
895
|
|
|
669
896
|
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
670
|
-
} catch (/** @type {
|
|
671
|
-
statusMessage(`Error: ${e.message}`);
|
|
897
|
+
} catch (/** @type {unknown} */ e) {
|
|
898
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Reload an already-open tab from disk without changing the active tab. Used to refresh after AI
|
|
904
|
+
* assistant writes to a file.
|
|
905
|
+
*
|
|
906
|
+
* @param {string} path
|
|
907
|
+
*/
|
|
908
|
+
export async function reloadFileInTab(path) {
|
|
909
|
+
for (const [, tab] of workspace.tabs.entries()) {
|
|
910
|
+
if (tab.documentPath === path) {
|
|
911
|
+
const platform = getPlatform();
|
|
912
|
+
try {
|
|
913
|
+
const content = await platform.readFile(path);
|
|
914
|
+
if (!content) return;
|
|
915
|
+
if (path.endsWith(".md")) {
|
|
916
|
+
const { document, frontmatter } = await loadMarkdown(content);
|
|
917
|
+
tab.doc.document = document;
|
|
918
|
+
tab.doc.content.frontmatter = frontmatter;
|
|
919
|
+
} else {
|
|
920
|
+
tab.doc.document = JSON.parse(content);
|
|
921
|
+
}
|
|
922
|
+
tab.doc.dirty = false;
|
|
923
|
+
} catch {}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
672
926
|
}
|
|
673
927
|
}
|