sh3-core 0.20.1 → 0.20.3
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/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/artifact.d.ts +2 -0
- package/dist/boot/satellitePayload.d.ts +2 -0
- package/dist/boot/satellitePayload.test.js +19 -0
- package/dist/build.d.ts +7 -1
- package/dist/build.js +22 -3
- package/dist/build.test.js +27 -1
- package/dist/createShell.js +34 -9
- package/dist/documents/backends.d.ts +12 -0
- package/dist/documents/backends.js +230 -3
- package/dist/documents/backends.test.js +147 -1
- package/dist/documents/browse.d.ts +20 -0
- package/dist/documents/browse.js +35 -0
- package/dist/documents/browse.test.js +125 -0
- package/dist/documents/config.d.ts +2 -4
- package/dist/documents/config.js +3 -7
- package/dist/documents/handle.js +40 -0
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +11 -0
- package/dist/documents/http-backend.js +86 -0
- package/dist/documents/http-backend.test.js +117 -1
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.test.js +2 -2
- package/dist/documents/types.d.ts +87 -14
- package/dist/documents/types.js +4 -0
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/layout/slotHostPool.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
- package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
- package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
- package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
- package/dist/projects/session-state.svelte.d.ts +3 -0
- package/dist/projects/session-state.svelte.js +25 -0
- package/dist/projects/session-state.test.js +43 -2
- package/dist/projects-shard/ProjectsSection.svelte +14 -18
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.test.js +2 -2
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/appActions.js +5 -2
- package/dist/shards/activate-browse.test.js +2 -2
- package/dist/shards/activate-contributions.test.js +2 -2
- package/dist/shards/activate-error-isolation.test.js +3 -3
- package/dist/shards/activate-on-key-revoked.test.js +2 -2
- package/dist/shards/activate-runtime.test.js +2 -2
- package/dist/shards/activate.svelte.js +5 -5
- package/dist/shards/ctx-fetch.test.js +4 -4
- package/dist/shell-shard/Terminal.svelte +4 -1
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
- package/dist/shell-shard/dispatch.d.ts +2 -0
- package/dist/shell-shard/dispatch.js +2 -0
- package/dist/shell-shard/manifest.js +7 -1
- package/dist/shell-shard/shellShard.svelte.js +1 -1
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +35 -0
- package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cat.test.js +49 -0
- package/dist/shell-shard/verbs/index.js +12 -0
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +48 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +64 -0
- package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
- package/dist/shell-shard/verbs/mkdir.js +30 -0
- package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mkdir.test.js +48 -0
- package/dist/shell-shard/verbs/mv.d.ts +2 -0
- package/dist/shell-shard/verbs/mv.js +33 -0
- package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mv.test.js +55 -0
- package/dist/shell-shard/verbs/rm.d.ts +2 -0
- package/dist/shell-shard/verbs/rm.js +28 -0
- package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
- package/dist/shell-shard/verbs/rm.test.js +47 -0
- package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
- package/dist/shell-shard/verbs/scope-parse.js +33 -0
- package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
- package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
- package/dist/shell-shard/verbs/xfer.d.ts +2 -0
- package/dist/shell-shard/verbs/xfer.js +87 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +107 -0
- package/dist/verbs/types.d.ts +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
type SaverValue,
|
|
12
12
|
type FileItem,
|
|
13
13
|
} from './DocumentFilePicker';
|
|
14
|
+
import Button from '../Button.svelte';
|
|
15
|
+
import FolderConfirmDelete from './_FolderConfirmDelete.svelte';
|
|
16
|
+
import { documentChanges } from '../../documents/notifications';
|
|
14
17
|
|
|
15
18
|
let {
|
|
16
19
|
mode,
|
|
@@ -19,6 +22,10 @@
|
|
|
19
22
|
onCancel,
|
|
20
23
|
close,
|
|
21
24
|
suggestedName = '',
|
|
25
|
+
selectable = 'file',
|
|
26
|
+
listFolders,
|
|
27
|
+
handle,
|
|
28
|
+
readOnlyShard,
|
|
22
29
|
}: {
|
|
23
30
|
mode: 'open' | 'save';
|
|
24
31
|
docs: DocEntry[];
|
|
@@ -26,16 +33,49 @@
|
|
|
26
33
|
onCancel: () => void;
|
|
27
34
|
close: () => void;
|
|
28
35
|
suggestedName?: string;
|
|
36
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
37
|
+
listFolders?: (shardId: string, prefix: string) => Promise<string[]>;
|
|
38
|
+
handle?: {
|
|
39
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
40
|
+
rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
|
|
41
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
42
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
43
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
29
46
|
} = $props();
|
|
30
47
|
|
|
48
|
+
type Selected =
|
|
49
|
+
| { kind: 'file'; doc: DocEntry }
|
|
50
|
+
| { kind: 'folder'; fullPath: string; name: string }
|
|
51
|
+
| null;
|
|
52
|
+
|
|
31
53
|
let shardId = $state<string | null>(null);
|
|
32
54
|
let prefix = $state('');
|
|
33
|
-
let
|
|
55
|
+
let selected = $state<Selected>(null);
|
|
34
56
|
let filename = $state(untrack(() => suggestedName));
|
|
35
57
|
let activeIdx = $state(0);
|
|
36
58
|
let listEl = $state<HTMLElement | undefined>(undefined);
|
|
37
59
|
|
|
38
|
-
|
|
60
|
+
// Folder state loaded via listFolders
|
|
61
|
+
let folders = $state<string[]>([]);
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
folders = [];
|
|
65
|
+
if (!listFolders || shardId === null) return;
|
|
66
|
+
const _shard = shardId;
|
|
67
|
+
const _prefix = prefix;
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const f = await listFolders(_shard, _prefix);
|
|
72
|
+
if (!cancelled) folders = f;
|
|
73
|
+
} catch { /* leave folders empty on error */ }
|
|
74
|
+
})();
|
|
75
|
+
return () => { cancelled = true; };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const items = $derived(buildTree(docs, folders, shardId, prefix));
|
|
39
79
|
const crumbs = $derived(breadcrumbSegments(shardId, prefix));
|
|
40
80
|
|
|
41
81
|
$effect(() => {
|
|
@@ -43,10 +83,33 @@
|
|
|
43
83
|
activeIdx = Math.min(activeIdx, items.length - 1);
|
|
44
84
|
});
|
|
45
85
|
|
|
86
|
+
const toolbarVisible = $derived(
|
|
87
|
+
shardId !== null && !!handle && !(readOnlyShard?.(shardId) ?? false),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Toolbar state
|
|
91
|
+
let newFolderActive = $state(false);
|
|
92
|
+
let newFolderName = $state('');
|
|
93
|
+
// Path key for rename: doc.path for files, fullPath for folders
|
|
94
|
+
let renamingPath = $state<string | null>(null);
|
|
95
|
+
let renameValue = $state('');
|
|
96
|
+
let toolbarError = $state<string | null>(null);
|
|
97
|
+
let confirmDelete = $state<FileItem | null>(null);
|
|
98
|
+
let clipboard = $state<
|
|
99
|
+
| { kind: 'file'; shardId: string; path: string }
|
|
100
|
+
| { kind: 'folder'; shardId: string; path: string }
|
|
101
|
+
| null
|
|
102
|
+
>(null);
|
|
103
|
+
|
|
104
|
+
function showError(msg: string) {
|
|
105
|
+
toolbarError = msg;
|
|
106
|
+
setTimeout(() => { if (toolbarError === msg) toolbarError = null; }, 3000);
|
|
107
|
+
}
|
|
108
|
+
|
|
46
109
|
function navigateShard(id: string) {
|
|
47
110
|
shardId = id;
|
|
48
111
|
prefix = '';
|
|
49
|
-
|
|
112
|
+
selected = null;
|
|
50
113
|
filename = '';
|
|
51
114
|
activeIdx = 0;
|
|
52
115
|
}
|
|
@@ -56,18 +119,22 @@
|
|
|
56
119
|
navigateShard(p);
|
|
57
120
|
} else {
|
|
58
121
|
prefix = p;
|
|
59
|
-
|
|
122
|
+
selected = null;
|
|
60
123
|
filename = '';
|
|
61
124
|
activeIdx = 0;
|
|
62
125
|
}
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
function
|
|
128
|
+
function selectItem(item: FileItem) {
|
|
66
129
|
if (item.kind === 'folder') {
|
|
67
|
-
|
|
130
|
+
if (selectable !== 'file') {
|
|
131
|
+
selected = { kind: 'folder', fullPath: item.fullPath, name: item.name };
|
|
132
|
+
} else {
|
|
133
|
+
navigatePrefix(item.fullPath);
|
|
134
|
+
}
|
|
68
135
|
} else {
|
|
69
136
|
if (mode === 'open') {
|
|
70
|
-
|
|
137
|
+
selected = { kind: 'file', doc: item.doc };
|
|
71
138
|
}
|
|
72
139
|
if (mode === 'save') {
|
|
73
140
|
filename = item.name;
|
|
@@ -76,8 +143,12 @@
|
|
|
76
143
|
}
|
|
77
144
|
|
|
78
145
|
function commit() {
|
|
79
|
-
if (mode === 'open' &&
|
|
80
|
-
|
|
146
|
+
if (mode === 'open' && selected) {
|
|
147
|
+
if (selected.kind === 'file') {
|
|
148
|
+
onCommit({ shardId: selected.doc.shardId, path: selected.doc.path, kind: 'file' });
|
|
149
|
+
} else {
|
|
150
|
+
onCommit({ shardId: shardId!, path: selected.fullPath, kind: 'folder' });
|
|
151
|
+
}
|
|
81
152
|
close();
|
|
82
153
|
} else if (mode === 'save' && filename.trim() && shardId) {
|
|
83
154
|
const p = prefix ? `${shardId}/${prefix}/${filename}` : `${shardId}/${filename}`;
|
|
@@ -92,10 +163,150 @@
|
|
|
92
163
|
}
|
|
93
164
|
|
|
94
165
|
function canCommit(): boolean {
|
|
95
|
-
if (mode === 'open')
|
|
166
|
+
if (mode === 'open') {
|
|
167
|
+
if (!selected) return false;
|
|
168
|
+
if (selectable === 'file') return selected.kind === 'file';
|
|
169
|
+
if (selectable === 'folder') return selected.kind === 'folder';
|
|
170
|
+
return true; // 'both'
|
|
171
|
+
}
|
|
96
172
|
return filename.trim().length > 0 && shardId !== null;
|
|
97
173
|
}
|
|
98
174
|
|
|
175
|
+
// Toolbar actions
|
|
176
|
+
function beginNewFolder() {
|
|
177
|
+
if (!handle || shardId === null) return;
|
|
178
|
+
newFolderActive = true;
|
|
179
|
+
newFolderName = '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function commitNewFolder() {
|
|
183
|
+
const name = newFolderName.trim();
|
|
184
|
+
newFolderActive = false;
|
|
185
|
+
if (!name || !handle || shardId === null) return;
|
|
186
|
+
if (items.some((i) => i.name === name)) {
|
|
187
|
+
showError(`"${name}" already exists`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
191
|
+
try {
|
|
192
|
+
await handle.mkdir(shardId, fullPath);
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
showError(String((err as Error)?.message ?? err));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cancelNewFolder() {
|
|
199
|
+
newFolderActive = false;
|
|
200
|
+
newFolderName = '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function beginRename() {
|
|
204
|
+
if (!selected) return;
|
|
205
|
+
if (selected.kind === 'file') {
|
|
206
|
+
renamingPath = (selected as { kind: 'file'; doc: DocEntry }).doc.path;
|
|
207
|
+
renameValue = (selected as { kind: 'file'; doc: DocEntry }).doc.path.split('/').pop() ?? '';
|
|
208
|
+
} else {
|
|
209
|
+
renamingPath = (selected as { kind: 'folder'; fullPath: string; name: string }).fullPath;
|
|
210
|
+
renameValue = (selected as { kind: 'folder'; fullPath: string; name: string }).name;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function commitRename() {
|
|
215
|
+
const path = renamingPath;
|
|
216
|
+
const newName = renameValue.trim();
|
|
217
|
+
renamingPath = null;
|
|
218
|
+
if (!path || !newName || !handle || shardId === null) return;
|
|
219
|
+
const item = items.find(
|
|
220
|
+
(i) => (i.kind === 'file' && i.doc.path === path) ||
|
|
221
|
+
(i.kind === 'folder' && i.fullPath === path),
|
|
222
|
+
);
|
|
223
|
+
if (!item || newName === item.name) return;
|
|
224
|
+
if (items.some((i) => i !== item && i.name === newName)) {
|
|
225
|
+
showError(`"${newName}" already exists`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
if (item.kind === 'file') {
|
|
230
|
+
const newPath = prefix ? `${prefix}/${newName}` : newName;
|
|
231
|
+
await handle.rename(shardId, item.doc.path, newPath);
|
|
232
|
+
} else {
|
|
233
|
+
const newFull = prefix ? `${prefix}/${newName}` : newName;
|
|
234
|
+
await handle.renameFolder(shardId, item.fullPath, newFull);
|
|
235
|
+
}
|
|
236
|
+
} catch (err: unknown) {
|
|
237
|
+
showError(String((err as Error)?.message ?? err));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cancelRename() { renamingPath = null; renameValue = ''; }
|
|
242
|
+
|
|
243
|
+
function beginDelete() {
|
|
244
|
+
if (!selected) return;
|
|
245
|
+
if (selected.kind === 'file') {
|
|
246
|
+
const path = (selected as { kind: 'file'; doc: DocEntry }).doc.path;
|
|
247
|
+
confirmDelete = items.find((i) => i.kind === 'file' && i.doc.path === path) ?? null;
|
|
248
|
+
} else {
|
|
249
|
+
const fullPath = (selected as { kind: 'folder'; fullPath: string; name: string }).fullPath;
|
|
250
|
+
confirmDelete = items.find((i) => i.kind === 'folder' && i.fullPath === fullPath) ?? null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function performDelete(recursive: boolean) {
|
|
255
|
+
const item = confirmDelete;
|
|
256
|
+
confirmDelete = null;
|
|
257
|
+
if (!item || !handle || shardId === null) return;
|
|
258
|
+
try {
|
|
259
|
+
if (item.kind === 'file') {
|
|
260
|
+
await handle.delete(shardId, item.doc.path);
|
|
261
|
+
} else {
|
|
262
|
+
await handle.rmdir(shardId, item.fullPath, { recursive });
|
|
263
|
+
}
|
|
264
|
+
selected = null;
|
|
265
|
+
} catch (err: unknown) {
|
|
266
|
+
showError(String((err as Error)?.message ?? err));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function cutSelected() {
|
|
271
|
+
if (!selected || shardId === null) return;
|
|
272
|
+
if (selected.kind === 'file') {
|
|
273
|
+
clipboard = { kind: 'file', shardId, path: selected.doc.path };
|
|
274
|
+
} else {
|
|
275
|
+
clipboard = { kind: 'folder', shardId, path: selected.fullPath };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function canPasteHere(): boolean {
|
|
280
|
+
if (!clipboard || !handle || shardId === null) return false;
|
|
281
|
+
if (clipboard.shardId !== shardId) return false;
|
|
282
|
+
const targetPrefix = prefix;
|
|
283
|
+
if (clipboard.kind === 'folder') {
|
|
284
|
+
if (targetPrefix === clipboard.path) return false;
|
|
285
|
+
if (targetPrefix.startsWith(clipboard.path + '/')) return false;
|
|
286
|
+
}
|
|
287
|
+
const sourceParent = clipboard.path.includes('/')
|
|
288
|
+
? clipboard.path.slice(0, clipboard.path.lastIndexOf('/'))
|
|
289
|
+
: '';
|
|
290
|
+
if (sourceParent === targetPrefix) return false;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function pasteHere() {
|
|
295
|
+
if (!canPasteHere() || !clipboard || !handle || shardId === null) return;
|
|
296
|
+
const name = clipboard.path.split('/').pop()!;
|
|
297
|
+
const newPath = prefix ? `${prefix}/${name}` : name;
|
|
298
|
+
try {
|
|
299
|
+
if (clipboard.kind === 'file') {
|
|
300
|
+
await handle.rename(shardId, clipboard.path, newPath);
|
|
301
|
+
} else {
|
|
302
|
+
await handle.renameFolder(shardId, clipboard.path, newPath);
|
|
303
|
+
}
|
|
304
|
+
clipboard = null;
|
|
305
|
+
} catch (err: unknown) {
|
|
306
|
+
showError(String((err as Error)?.message ?? err));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
99
310
|
function onKey(e: KeyboardEvent) {
|
|
100
311
|
if (e.target instanceof HTMLInputElement) return;
|
|
101
312
|
switch (e.key) {
|
|
@@ -110,13 +321,30 @@
|
|
|
110
321
|
case 'Enter':
|
|
111
322
|
e.preventDefault();
|
|
112
323
|
if (activeIdx >= 0 && activeIdx < items.length) {
|
|
113
|
-
|
|
324
|
+
selectItem(items[activeIdx]);
|
|
114
325
|
}
|
|
115
326
|
break;
|
|
116
327
|
case 'Escape':
|
|
117
328
|
e.preventDefault();
|
|
118
329
|
cancel();
|
|
119
330
|
break;
|
|
331
|
+
case 'F2':
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
beginRename();
|
|
334
|
+
break;
|
|
335
|
+
case 'Delete':
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
beginDelete();
|
|
338
|
+
break;
|
|
339
|
+
case 'x':
|
|
340
|
+
if (e.ctrlKey || e.metaKey) { e.preventDefault(); cutSelected(); }
|
|
341
|
+
break;
|
|
342
|
+
case 'v':
|
|
343
|
+
if (e.ctrlKey || e.metaKey) { e.preventDefault(); void pasteHere(); }
|
|
344
|
+
break;
|
|
345
|
+
case 'N':
|
|
346
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey) { e.preventDefault(); beginNewFolder(); }
|
|
347
|
+
break;
|
|
120
348
|
case 'Backspace':
|
|
121
349
|
if (prefix) {
|
|
122
350
|
e.preventDefault();
|
|
@@ -137,10 +365,21 @@
|
|
|
137
365
|
if (item.kind === 'folder') {
|
|
138
366
|
navigatePrefix(item.fullPath);
|
|
139
367
|
} else if (mode === 'open') {
|
|
140
|
-
onCommit({ shardId: item.doc.shardId, path: item.doc.path });
|
|
368
|
+
onCommit({ shardId: item.doc.shardId, path: item.doc.path, kind: 'file' });
|
|
141
369
|
close();
|
|
142
370
|
}
|
|
143
371
|
}
|
|
372
|
+
|
|
373
|
+
// Live folder refresh on document changes
|
|
374
|
+
$effect(() => {
|
|
375
|
+
const unsub = documentChanges.subscribe(async (change) => {
|
|
376
|
+
if (shardId !== null && change.shardId !== shardId) return;
|
|
377
|
+
if (listFolders && shardId !== null) {
|
|
378
|
+
try { folders = await listFolders(shardId, prefix); } catch { /* ignore */ }
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return () => unsub();
|
|
382
|
+
});
|
|
144
383
|
</script>
|
|
145
384
|
|
|
146
385
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -161,7 +400,7 @@
|
|
|
161
400
|
onclick={() => {
|
|
162
401
|
shardId = seg.targetShard;
|
|
163
402
|
prefix = seg.targetPrefix;
|
|
164
|
-
|
|
403
|
+
selected = null;
|
|
165
404
|
filename = '';
|
|
166
405
|
activeIdx = 0;
|
|
167
406
|
}}
|
|
@@ -169,15 +408,100 @@
|
|
|
169
408
|
{/each}
|
|
170
409
|
</nav>
|
|
171
410
|
|
|
411
|
+
{#if toolbarVisible}
|
|
412
|
+
<div class="sh3-doc-browser__toolbar">
|
|
413
|
+
<Button
|
|
414
|
+
variant="icon"
|
|
415
|
+
icon="folder-plus"
|
|
416
|
+
title="New folder"
|
|
417
|
+
onclick={beginNewFolder}
|
|
418
|
+
disabled={newFolderActive}
|
|
419
|
+
/>
|
|
420
|
+
<Button
|
|
421
|
+
variant="icon"
|
|
422
|
+
icon="pencil"
|
|
423
|
+
title="Rename"
|
|
424
|
+
onclick={beginRename}
|
|
425
|
+
disabled={!selected}
|
|
426
|
+
/>
|
|
427
|
+
<Button
|
|
428
|
+
variant="icon"
|
|
429
|
+
icon="trash-2"
|
|
430
|
+
title="Delete"
|
|
431
|
+
onclick={beginDelete}
|
|
432
|
+
disabled={!selected}
|
|
433
|
+
/>
|
|
434
|
+
<Button
|
|
435
|
+
variant="icon"
|
|
436
|
+
icon="scissors"
|
|
437
|
+
title="Cut"
|
|
438
|
+
onclick={cutSelected}
|
|
439
|
+
disabled={!selected}
|
|
440
|
+
/>
|
|
441
|
+
<Button
|
|
442
|
+
variant="icon"
|
|
443
|
+
icon="clipboard"
|
|
444
|
+
title="Paste"
|
|
445
|
+
onclick={() => void pasteHere()}
|
|
446
|
+
disabled={!canPasteHere()}
|
|
447
|
+
/>
|
|
448
|
+
{#if toolbarError}
|
|
449
|
+
<span class="sh3-doc-browser__toolbar-error">{toolbarError}</span>
|
|
450
|
+
{/if}
|
|
451
|
+
</div>
|
|
452
|
+
{/if}
|
|
453
|
+
|
|
172
454
|
<div class="sh3-doc-browser__list" bind:this={listEl}>
|
|
173
|
-
{#if
|
|
174
|
-
|
|
175
|
-
|
|
455
|
+
{#if confirmDelete}
|
|
456
|
+
{@const childCount = confirmDelete.kind === 'folder'
|
|
457
|
+
? docs.filter((d) =>
|
|
458
|
+
d.shardId === shardId && d.path.startsWith(confirmDelete!.fullPath + '/'),
|
|
459
|
+
).length
|
|
460
|
+
: 0}
|
|
461
|
+
<div class="sh3-doc-browser__confirm-overlay">
|
|
462
|
+
<FolderConfirmDelete
|
|
463
|
+
item={{ kind: confirmDelete.kind, name: confirmDelete.name }}
|
|
464
|
+
{childCount}
|
|
465
|
+
onConfirm={() => void performDelete(confirmDelete!.kind === 'folder')}
|
|
466
|
+
onCancel={() => { confirmDelete = null; }}
|
|
467
|
+
/>
|
|
176
468
|
</div>
|
|
177
469
|
{:else}
|
|
470
|
+
{#if items.length === 0 && !newFolderActive}
|
|
471
|
+
<div class="sh3-doc-browser__empty">
|
|
472
|
+
{shardId === null ? 'No shards available.' : prefix ? 'Empty directory.' : 'No documents in this shard.'}
|
|
473
|
+
</div>
|
|
474
|
+
{/if}
|
|
475
|
+
|
|
476
|
+
{#if newFolderActive}
|
|
477
|
+
<div class="sh3-doc-browser__item sh3-doc-browser__item--folder sh3-doc-browser__item--editing">
|
|
478
|
+
<span class="sh3-doc-browser__icon sh3-doc-browser__icon--folder" aria-hidden="true">📁</span>
|
|
479
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
480
|
+
<input
|
|
481
|
+
class="sh3-doc-browser__rename-input"
|
|
482
|
+
type="text"
|
|
483
|
+
autofocus
|
|
484
|
+
bind:value={newFolderName}
|
|
485
|
+
onkeydown={(e) => {
|
|
486
|
+
if (e.key === 'Enter') { e.preventDefault(); void commitNewFolder(); }
|
|
487
|
+
if (e.key === 'Escape') { e.preventDefault(); cancelNewFolder(); }
|
|
488
|
+
}}
|
|
489
|
+
onblur={() => void commitNewFolder()}
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
{/if}
|
|
493
|
+
|
|
178
494
|
{#each items as item, i}
|
|
179
495
|
{@const isActive = i === activeIdx}
|
|
180
|
-
{@const isSelected =
|
|
496
|
+
{@const isSelected =
|
|
497
|
+
(item.kind === 'file' && mode === 'open' && selected?.kind === 'file' &&
|
|
498
|
+
selected.doc.path === item.doc.path && selected.doc.shardId === item.doc.shardId) ||
|
|
499
|
+
(item.kind === 'folder' && selected?.kind === 'folder' &&
|
|
500
|
+
selected.fullPath === item.fullPath)}
|
|
501
|
+
{@const isRenaming = renamingPath !== null && (
|
|
502
|
+
(item.kind === 'file' && item.doc.path === renamingPath) ||
|
|
503
|
+
(item.kind === 'folder' && item.fullPath === renamingPath)
|
|
504
|
+
)}
|
|
181
505
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
182
506
|
<div
|
|
183
507
|
class="sh3-doc-browser__item"
|
|
@@ -187,22 +511,52 @@
|
|
|
187
511
|
role="option"
|
|
188
512
|
tabindex="-1"
|
|
189
513
|
aria-selected={isSelected}
|
|
190
|
-
onclick={() =>
|
|
191
|
-
onkeydown={(e) => { if (e.key === 'Enter')
|
|
514
|
+
onclick={() => selectItem(item)}
|
|
515
|
+
onkeydown={(e) => { if (e.key === 'Enter') selectItem(item); }}
|
|
192
516
|
ondblclick={() => onDblClick(item)}
|
|
193
517
|
onmouseenter={() => activeIdx = i}
|
|
194
518
|
>
|
|
195
519
|
{#if item.kind === 'folder'}
|
|
196
520
|
<span class="sh3-doc-browser__icon sh3-doc-browser__icon--folder" aria-hidden="true">📁</span>
|
|
197
|
-
|
|
198
|
-
|
|
521
|
+
{#if isRenaming}
|
|
522
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
523
|
+
<input
|
|
524
|
+
class="sh3-doc-browser__rename-input"
|
|
525
|
+
type="text"
|
|
526
|
+
autofocus
|
|
527
|
+
bind:value={renameValue}
|
|
528
|
+
onkeydown={(e) => {
|
|
529
|
+
if (e.key === 'Enter') { e.preventDefault(); void commitRename(); }
|
|
530
|
+
if (e.key === 'Escape') { e.preventDefault(); cancelRename(); }
|
|
531
|
+
}}
|
|
532
|
+
onblur={() => void commitRename()}
|
|
533
|
+
/>
|
|
534
|
+
{:else}
|
|
535
|
+
<span class="sh3-doc-browser__name">{item.name}</span>
|
|
536
|
+
<span class="sh3-doc-browser__meta"></span>
|
|
537
|
+
{/if}
|
|
199
538
|
{:else}
|
|
200
539
|
<span class="sh3-doc-browser__icon" aria-hidden="true">{iconForFile(item.name)}</span>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
540
|
+
{#if isRenaming}
|
|
541
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
542
|
+
<input
|
|
543
|
+
class="sh3-doc-browser__rename-input"
|
|
544
|
+
type="text"
|
|
545
|
+
autofocus
|
|
546
|
+
bind:value={renameValue}
|
|
547
|
+
onkeydown={(e) => {
|
|
548
|
+
if (e.key === 'Enter') { e.preventDefault(); void commitRename(); }
|
|
549
|
+
if (e.key === 'Escape') { e.preventDefault(); cancelRename(); }
|
|
550
|
+
}}
|
|
551
|
+
onblur={() => void commitRename()}
|
|
552
|
+
/>
|
|
553
|
+
{:else}
|
|
554
|
+
<span class="sh3-doc-browser__name">{item.name}</span>
|
|
555
|
+
<span class="sh3-doc-browser__meta">
|
|
556
|
+
{formatSize(item.doc.size)}
|
|
557
|
+
<span class="sh3-doc-browser__date">{formatDate(item.doc.lastModified)}</span>
|
|
558
|
+
</span>
|
|
559
|
+
{/if}
|
|
206
560
|
{/if}
|
|
207
561
|
</div>
|
|
208
562
|
{/each}
|
|
@@ -272,11 +626,33 @@
|
|
|
272
626
|
.sh3-doc-browser__crumb:hover { background: var(--sh3-bg); color: var(--sh3-fg); }
|
|
273
627
|
.sh3-doc-browser__crumb--last { color: var(--sh3-fg); font-weight: 600; }
|
|
274
628
|
.sh3-doc-browser__crumb-sep { color: var(--sh3-fg-subtle); font-size: 0.75rem; flex-shrink: 0; }
|
|
275
|
-
.sh3-doc-
|
|
629
|
+
.sh3-doc-browser__toolbar {
|
|
630
|
+
display: flex; align-items: center; gap: 2px;
|
|
631
|
+
padding: 2px 8px;
|
|
632
|
+
border-bottom: 1px solid var(--sh3-border);
|
|
633
|
+
flex-shrink: 0;
|
|
634
|
+
}
|
|
635
|
+
.sh3-doc-browser__toolbar-error {
|
|
636
|
+
font-size: 0.6875rem;
|
|
637
|
+
color: var(--sh3-error);
|
|
638
|
+
padding: 0 4px;
|
|
639
|
+
flex: 1;
|
|
640
|
+
overflow: hidden;
|
|
641
|
+
text-overflow: ellipsis;
|
|
642
|
+
white-space: nowrap;
|
|
643
|
+
}
|
|
644
|
+
.sh3-doc-browser__list { flex: 1; overflow-y: auto; padding: 4px 0; min-height: 0; position: relative; }
|
|
645
|
+
.sh3-doc-browser__confirm-overlay {
|
|
646
|
+
position: absolute; inset: 0;
|
|
647
|
+
display: flex; align-items: center; justify-content: center;
|
|
648
|
+
background: var(--sh3-bg-overlay, rgba(0,0,0,0.3));
|
|
649
|
+
z-index: 1;
|
|
650
|
+
}
|
|
276
651
|
.sh3-doc-browser__item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; cursor: pointer; }
|
|
277
652
|
.sh3-doc-browser__item--active { background: var(--sh3-bg); }
|
|
278
653
|
.sh3-doc-browser__item--selected { background: var(--sh3-accent); color: var(--sh3-fg-on-accent); }
|
|
279
654
|
.sh3-doc-browser__item--active.sh3-doc-browser__item--selected { background: var(--sh3-accent); }
|
|
655
|
+
.sh3-doc-browser__item--editing { cursor: default; }
|
|
280
656
|
.sh3-doc-browser__icon { flex-shrink: 0; width: 18px; text-align: center; font-size: 0.875rem; }
|
|
281
657
|
.sh3-doc-browser__icon--folder { font-size: 0.75rem; }
|
|
282
658
|
.sh3-doc-browser__name {
|
|
@@ -285,6 +661,17 @@
|
|
|
285
661
|
font-family: var(--sh3-font-mono); font-size: 0.75rem;
|
|
286
662
|
}
|
|
287
663
|
.sh3-doc-browser__item--folder .sh3-doc-browser__name { font-family: inherit; font-size: 0.8125rem; }
|
|
664
|
+
.sh3-doc-browser__rename-input {
|
|
665
|
+
flex: 1; min-width: 0;
|
|
666
|
+
height: 22px;
|
|
667
|
+
padding: 0 4px;
|
|
668
|
+
background: var(--sh3-input-bg);
|
|
669
|
+
border: 1px solid var(--sh3-input-border-focus);
|
|
670
|
+
border-radius: var(--sh3-radius-sm);
|
|
671
|
+
color: var(--sh3-fg);
|
|
672
|
+
font: inherit; font-size: 0.75rem;
|
|
673
|
+
outline: none;
|
|
674
|
+
}
|
|
288
675
|
.sh3-doc-browser__meta {
|
|
289
676
|
display: flex; align-items: center; gap: 10px;
|
|
290
677
|
flex-shrink: 0; font-size: 0.6875rem; color: var(--sh3-fg-muted);
|
|
@@ -6,6 +6,18 @@ type $$ComponentProps = {
|
|
|
6
6
|
onCancel: () => void;
|
|
7
7
|
close: () => void;
|
|
8
8
|
suggestedName?: string;
|
|
9
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
10
|
+
listFolders?: (shardId: string, prefix: string) => Promise<string[]>;
|
|
11
|
+
handle?: {
|
|
12
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
13
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
14
|
+
recursive: boolean;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
17
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
18
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
9
21
|
};
|
|
10
22
|
declare const DocumentBrowser: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
23
|
type DocumentBrowser = ReturnType<typeof DocumentBrowser>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|