@jxsuite/studio 0.20.0 → 0.21.1
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 +1229 -477
- package/dist/studio.js.map +41 -39
- package/package.json +6 -5
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +19 -16
- package/src/canvas/canvas-live-render.js +19 -0
- package/src/canvas/canvas-utils.js +13 -6
- package/src/editor/context-menu.js +27 -13
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +9 -10
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +3 -0
- package/src/editor/shortcuts.js +10 -2
- package/src/editor/slash-menu.js +36 -17
- package/src/files/files.js +244 -16
- package/src/github/github-publish.js +26 -15
- package/src/panels/activity-bar.js +5 -5
- package/src/panels/ai-panel.js +10 -3
- package/src/panels/block-action-bar.js +73 -24
- package/src/panels/git-panel.js +7 -2
- package/src/panels/imports-panel.js +6 -1
- package/src/panels/left-panel.js +20 -1
- package/src/panels/properties-panel.js +16 -11
- package/src/panels/quick-search.js +4 -4
- package/src/panels/right-panel.js +16 -14
- package/src/panels/signals-panel.js +120 -0
- package/src/panels/statusbar.js +6 -2
- package/src/panels/style-panel.js +6 -2
- package/src/panels/tab-strip.js +5 -2
- package/src/settings/content-types-editor.js +1 -1
- package/src/settings/settings-modal.js +12 -9
- package/src/store.js +15 -9
- package/src/studio.js +17 -9
- package/src/ui/layers.js +31 -1
- package/src/utils/edit-display.js +1 -6
- package/src/utils/studio-utils.js +11 -11
- package/src/workspace/workspace.js +19 -0
package/src/editor/slash-menu.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, render as litRender, nothing } from "lit-html";
|
|
11
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
11
12
|
import { getLayerSlot } from "../ui/layers.js";
|
|
12
13
|
|
|
13
14
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
@@ -34,7 +35,13 @@ const SLASH_COMMANDS = [
|
|
|
34
35
|
|
|
35
36
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* @type {{
|
|
40
|
+
* onSelect: (cmd: SlashCommand) => void;
|
|
41
|
+
* showFilter?: boolean;
|
|
42
|
+
* commands?: SlashCommand[];
|
|
43
|
+
* } | null}
|
|
44
|
+
*/
|
|
38
45
|
let callbacks = null;
|
|
39
46
|
let activeIdx = 0;
|
|
40
47
|
/** @type {SlashCommand[]} */
|
|
@@ -44,6 +51,10 @@ let open = false;
|
|
|
44
51
|
let _anchorEl = null;
|
|
45
52
|
/** @type {DOMRect | null} */
|
|
46
53
|
let _anchorRect = null;
|
|
54
|
+
/** @type {HTMLInputElement | null} */
|
|
55
|
+
let _filterEl = null;
|
|
56
|
+
/** @type {HTMLElement | null} */
|
|
57
|
+
let _popoverEl = null;
|
|
47
58
|
|
|
48
59
|
/** @returns {HTMLElement} */
|
|
49
60
|
function getHost() {
|
|
@@ -62,18 +73,23 @@ export function isSlashMenuOpen() {
|
|
|
62
73
|
*
|
|
63
74
|
* @param {HTMLElement} anchorEl — the element being edited (for positioning)
|
|
64
75
|
* @param {string} filter — current typed filter text (after the "/")
|
|
65
|
-
* @param {{
|
|
76
|
+
* @param {{
|
|
77
|
+
* onSelect: (cmd: SlashCommand) => void;
|
|
78
|
+
* showFilter?: boolean;
|
|
79
|
+
* commands?: SlashCommand[];
|
|
80
|
+
* }} cbs
|
|
66
81
|
*/
|
|
67
82
|
export function showSlashMenu(anchorEl, filter, cbs) {
|
|
68
83
|
callbacks = cbs;
|
|
69
84
|
_anchorEl = anchorEl;
|
|
70
85
|
_anchorRect = anchorEl.getBoundingClientRect();
|
|
71
86
|
|
|
87
|
+
const source = cbs.commands || SLASH_COMMANDS;
|
|
72
88
|
filteredItems = filter
|
|
73
|
-
?
|
|
89
|
+
? source.filter(
|
|
74
90
|
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
75
91
|
)
|
|
76
|
-
:
|
|
92
|
+
: source;
|
|
77
93
|
|
|
78
94
|
if (!filteredItems.length && !cbs.showFilter) {
|
|
79
95
|
dismissSlashMenu();
|
|
@@ -94,10 +110,7 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
94
110
|
|
|
95
111
|
if (cbs.showFilter) {
|
|
96
112
|
requestAnimationFrame(() => {
|
|
97
|
-
|
|
98
|
-
getHost().querySelector(".slash-filter")
|
|
99
|
-
);
|
|
100
|
-
if (input) input.focus();
|
|
113
|
+
if (_filterEl) _filterEl.focus();
|
|
101
114
|
});
|
|
102
115
|
}
|
|
103
116
|
}
|
|
@@ -108,6 +121,8 @@ export function dismissSlashMenu() {
|
|
|
108
121
|
callbacks = null;
|
|
109
122
|
_anchorEl = null;
|
|
110
123
|
_anchorRect = null;
|
|
124
|
+
_filterEl = null;
|
|
125
|
+
_popoverEl = null;
|
|
111
126
|
filteredItems = [];
|
|
112
127
|
document.removeEventListener("keydown", onKeydown, true);
|
|
113
128
|
document.removeEventListener("mousedown", onOutsideClick, true);
|
|
@@ -127,6 +142,9 @@ function render(anchorEl, showFilter) {
|
|
|
127
142
|
html`
|
|
128
143
|
<sp-popover
|
|
129
144
|
open
|
|
145
|
+
${ref((el) => {
|
|
146
|
+
_popoverEl = /** @type {HTMLElement | null} */ (el || null);
|
|
147
|
+
})}
|
|
130
148
|
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
131
149
|
4}px;z-index:9999;max-height:320px;overflow-y:auto"
|
|
132
150
|
>
|
|
@@ -137,6 +155,9 @@ function render(anchorEl, showFilter) {
|
|
|
137
155
|
placeholder="Filter…"
|
|
138
156
|
autocomplete="off"
|
|
139
157
|
style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
|
|
158
|
+
${ref((el) => {
|
|
159
|
+
_filterEl = /** @type {HTMLInputElement | null} */ (el || null);
|
|
160
|
+
})}
|
|
140
161
|
@input=${onFilterInput}
|
|
141
162
|
/>`
|
|
142
163
|
: nothing}
|
|
@@ -169,9 +190,7 @@ function render(anchorEl, showFilter) {
|
|
|
169
190
|
|
|
170
191
|
/** @param {MouseEvent} e */
|
|
171
192
|
function onOutsideClick(e) {
|
|
172
|
-
|
|
173
|
-
const popover = host.querySelector("sp-popover");
|
|
174
|
-
if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
|
|
193
|
+
if (_popoverEl && !_popoverEl.contains(/** @type {Node} */ (e.target))) {
|
|
175
194
|
dismissSlashMenu();
|
|
176
195
|
}
|
|
177
196
|
}
|
|
@@ -188,21 +207,21 @@ function onFilterInput(e) {
|
|
|
188
207
|
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
189
208
|
const filter = input.value.toLowerCase();
|
|
190
209
|
|
|
210
|
+
const source = callbacks?.commands || SLASH_COMMANDS;
|
|
191
211
|
filteredItems = filter
|
|
192
|
-
?
|
|
212
|
+
? source.filter(
|
|
193
213
|
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
194
214
|
)
|
|
195
|
-
:
|
|
215
|
+
: source;
|
|
196
216
|
|
|
197
217
|
activeIdx = 0;
|
|
198
218
|
if (_anchorEl) render(_anchorEl, true);
|
|
199
219
|
|
|
200
220
|
// Re-focus input after re-render
|
|
201
221
|
requestAnimationFrame(() => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
el.selectionStart = el.selectionEnd = el.value.length;
|
|
222
|
+
if (_filterEl && _filterEl !== document.activeElement) {
|
|
223
|
+
_filterEl.focus();
|
|
224
|
+
_filterEl.selectionStart = _filterEl.selectionEnd = _filterEl.value.length;
|
|
206
225
|
}
|
|
207
226
|
});
|
|
208
227
|
}
|
package/src/files/files.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
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";
|
|
@@ -17,10 +18,17 @@ import { createState, projectState, setProjectState, requireProjectState } from
|
|
|
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";
|
|
@@ -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 ────────────────────────────────────────────────────────────────
|
|
@@ -509,11 +684,57 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
|
|
|
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("/")
|
|
@@ -527,6 +748,9 @@ async function renameFile(
|
|
|
527
748
|
if (requireProjectState().selectedPath === entry.path) {
|
|
528
749
|
requireProjectState().selectedPath = newPath;
|
|
529
750
|
}
|
|
751
|
+
if (workspace.tabs.has(entry.path)) {
|
|
752
|
+
renameTab(entry.path, newPath, newPath);
|
|
753
|
+
}
|
|
530
754
|
renderLeftPanel();
|
|
531
755
|
statusMessage(`Renamed to ${newName}`);
|
|
532
756
|
} catch (/** @type {unknown} */ e) {
|
|
@@ -538,7 +762,11 @@ 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);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Publish a local project to GitHub — creates a new repo and pushes. */
|
|
2
2
|
|
|
3
3
|
import { html } from "lit-html";
|
|
4
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
4
5
|
import { showDialog } from "../ui/layers.js";
|
|
5
6
|
import { authenticateGithub } from "./github-auth.js";
|
|
6
7
|
import { getPlatform } from "../platform.js";
|
|
@@ -19,27 +20,24 @@ export async function publishToGithub({ projectName }) {
|
|
|
19
20
|
if (!token) return false;
|
|
20
21
|
|
|
21
22
|
const repoOpts = await showDialog((done) => {
|
|
23
|
+
/** @type {HTMLInputElement | null} */
|
|
24
|
+
let _nameInput = null;
|
|
25
|
+
/** @type {HTMLInputElement | null} */
|
|
26
|
+
let _descInput = null;
|
|
27
|
+
/** @type {HTMLInputElement | null} */
|
|
28
|
+
let _privateToggle = null;
|
|
29
|
+
|
|
22
30
|
return html`
|
|
23
31
|
<sp-dialog-wrapper
|
|
24
32
|
open
|
|
25
33
|
headline="Publish to GitHub"
|
|
26
34
|
confirm-label="Create Repository"
|
|
27
35
|
cancel-label="Cancel"
|
|
28
|
-
@confirm=${(
|
|
29
|
-
const dialog = /** @type {HTMLElement} */ (e.target);
|
|
30
|
-
const nameInput = /** @type {HTMLInputElement | null} */ (
|
|
31
|
-
dialog.querySelector('[name="repo-name"]')
|
|
32
|
-
);
|
|
33
|
-
const descInput = /** @type {HTMLInputElement | null} */ (
|
|
34
|
-
dialog.querySelector('[name="repo-desc"]')
|
|
35
|
-
);
|
|
36
|
-
const privateToggle = /** @type {HTMLInputElement | null} */ (
|
|
37
|
-
dialog.querySelector('[name="repo-private"]')
|
|
38
|
-
);
|
|
36
|
+
@confirm=${() => {
|
|
39
37
|
done({
|
|
40
|
-
name:
|
|
41
|
-
description:
|
|
42
|
-
isPrivate:
|
|
38
|
+
name: _nameInput?.value || projectName,
|
|
39
|
+
description: _descInput?.value || "",
|
|
40
|
+
isPrivate: _privateToggle?.checked ?? true,
|
|
43
41
|
});
|
|
44
42
|
}}
|
|
45
43
|
@cancel=${() => done(null)}
|
|
@@ -52,6 +50,9 @@ export async function publishToGithub({ projectName }) {
|
|
|
52
50
|
name="repo-name"
|
|
53
51
|
value="${projectName}"
|
|
54
52
|
placeholder="my-project"
|
|
53
|
+
${ref((el) => {
|
|
54
|
+
_nameInput = /** @type {HTMLInputElement | null} */ (el || null);
|
|
55
|
+
})}
|
|
55
56
|
></sp-textfield>
|
|
56
57
|
|
|
57
58
|
<sp-field-label for="repo-desc">Description (optional)</sp-field-label>
|
|
@@ -59,10 +60,20 @@ export async function publishToGithub({ projectName }) {
|
|
|
59
60
|
id="repo-desc"
|
|
60
61
|
name="repo-desc"
|
|
61
62
|
placeholder="A brief description"
|
|
63
|
+
${ref((el) => {
|
|
64
|
+
_descInput = /** @type {HTMLInputElement | null} */ (el || null);
|
|
65
|
+
})}
|
|
62
66
|
></sp-textfield>
|
|
63
67
|
|
|
64
68
|
<sp-field-label>Visibility</sp-field-label>
|
|
65
|
-
<sp-switch
|
|
69
|
+
<sp-switch
|
|
70
|
+
name="repo-private"
|
|
71
|
+
checked
|
|
72
|
+
${ref((el) => {
|
|
73
|
+
_privateToggle = /** @type {HTMLInputElement | null} */ (el || null);
|
|
74
|
+
})}
|
|
75
|
+
>Private repository</sp-switch
|
|
76
|
+
>
|
|
66
77
|
</div>
|
|
67
78
|
</sp-dialog-wrapper>
|
|
68
79
|
`;
|
|
@@ -16,10 +16,11 @@ export function mount() {
|
|
|
16
16
|
_scope.run(() => {
|
|
17
17
|
effect(() => {
|
|
18
18
|
const tab = activeTab.value;
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (tab) {
|
|
20
|
+
const gs = tab.session.ui.gitStatus;
|
|
21
|
+
if (!gs && !tab.session.ui.gitLoading) {
|
|
22
|
+
refreshGitStatus();
|
|
23
|
+
}
|
|
23
24
|
}
|
|
24
25
|
renderActivityBar();
|
|
25
26
|
});
|
|
@@ -92,7 +93,6 @@ export function tabIcon(tag, size) {
|
|
|
92
93
|
|
|
93
94
|
export function renderActivityBar() {
|
|
94
95
|
const tab = activeTab.value;
|
|
95
|
-
if (!tab) return;
|
|
96
96
|
const leftTab = view.leftTab;
|
|
97
97
|
const gitFileCount = tab?.session.ui.gitStatus?.files?.length || 0;
|
|
98
98
|
const tabs = [
|
package/src/panels/ai-panel.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, nothing } from "lit-html";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
8
9
|
import quikchat from "quikchat/md";
|
|
9
10
|
import { getPlatform } from "../platform.js";
|
|
10
|
-
import { rightPanel } from "../store.js";
|
|
11
11
|
import { reloadFileInTab } from "../files/files.js";
|
|
12
12
|
|
|
13
13
|
// ─── State (module-level, persists across tab switches) ─────────────────────
|
|
@@ -34,6 +34,8 @@ let mounted = false;
|
|
|
34
34
|
let chatInstance = null;
|
|
35
35
|
/** @type {Element | null} */
|
|
36
36
|
let chatContainerEl = null;
|
|
37
|
+
/** @type {HTMLElement | null} */
|
|
38
|
+
let _quikChatEl = null;
|
|
37
39
|
let currentStreamMsgId = /** @type {number | null} */ (null);
|
|
38
40
|
let streamStarted = false;
|
|
39
41
|
/** @type {Set<string>} */
|
|
@@ -61,7 +63,7 @@ export function registerRightPanelRender(/** @type {Function} */ fn) {
|
|
|
61
63
|
// ─── QuikChat Mount ────────────────────────────────────────────────────────
|
|
62
64
|
|
|
63
65
|
export function mountQuikChat() {
|
|
64
|
-
const container =
|
|
66
|
+
const container = _quikChatEl;
|
|
65
67
|
if (!container) return;
|
|
66
68
|
if (chatInstance && chatContainerEl === container) return;
|
|
67
69
|
|
|
@@ -393,7 +395,12 @@ export function renderAiPanelTemplate() {
|
|
|
393
395
|
New Chat
|
|
394
396
|
</sp-action-button>
|
|
395
397
|
</div>
|
|
396
|
-
<div
|
|
398
|
+
<div
|
|
399
|
+
id="ai-quikchat"
|
|
400
|
+
${ref((el) => {
|
|
401
|
+
_quikChatEl = /** @type {HTMLElement | null} */ (el || null);
|
|
402
|
+
})}
|
|
403
|
+
></div>
|
|
397
404
|
</div>
|
|
398
405
|
`;
|
|
399
406
|
}
|