sh3-core 0.19.6 → 0.20.2
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/app/admin/AuthSettingsView.svelte +3 -9
- package/dist/app/admin/MountsView.svelte +276 -0
- package/dist/app/admin/MountsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +6 -6
- package/dist/app/admin/UsersView.svelte +103 -7
- package/dist/app/admin/adminApp.js +1 -0
- package/dist/app/admin/adminShard.svelte.js +10 -0
- package/dist/apps/lifecycle.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +1 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/auth/admin-users.svelte.js +2 -1
- package/dist/auth/auth.svelte.d.ts +4 -5
- package/dist/auth/auth.svelte.js +5 -6
- package/dist/auth/types.d.ts +0 -2
- package/dist/chrome/CompactChrome.svelte +25 -6
- package/dist/chrome/FloatsSheet.svelte +7 -32
- package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
- package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
- package/dist/chrome/MenuSheet.svelte +154 -148
- package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
- package/dist/chrome/MenuSheet.svelte.test.js +24 -12
- package/dist/createShell.js +32 -21
- package/dist/createShell.remoteAuth.test.js +9 -3
- 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 +18 -1
- package/dist/documents/browse.js +40 -7
- package/dist/documents/browse.test.js +35 -35
- package/dist/documents/config.d.ts +6 -0
- package/dist/documents/config.js +18 -1
- package/dist/documents/handle.js +65 -17
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +6 -0
- package/dist/documents/http-backend.js +71 -2
- package/dist/documents/http-backend.test.js +51 -1
- package/dist/documents/index.d.ts +2 -2
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.d.ts +4 -2
- package/dist/documents/picker-api.test.d.ts +1 -1
- package/dist/documents/picker-api.test.js +89 -59
- package/dist/documents/picker-primitive.d.ts +4 -0
- package/dist/documents/picker-primitive.js +27 -29
- package/dist/documents/types.d.ts +93 -19
- package/dist/documents/types.js +6 -0
- package/dist/layout/presets.test.js +4 -4
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutsSection.svelte +3 -16
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
- 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/PickerList.svelte +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
- 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-shard/DeleteProjectDialog.svelte +32 -1
- package/dist/projects-shard/ProjectManage.svelte +197 -28
- package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
- package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
- package/dist/projects-shard/ProjectsSection.svelte +3 -16
- package/dist/projects-shard/projectsApi.js +2 -1
- package/dist/registry/permission-descriptions.js +4 -0
- package/dist/server-shard/types.d.ts +21 -0
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/HomeSection.svelte +107 -0
- package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
- package/dist/sh3core-shard/Sh3Home.svelte +9 -23
- package/dist/shards/activate.svelte.d.ts +4 -0
- package/dist/shards/activate.svelte.js +11 -3
- package/dist/shards/types.d.ts +7 -0
- 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/tenant-fs-client.js +2 -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 +101 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +96 -0
- package/dist/transport/apiFetch.js +12 -5
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import {
|
|
3
4
|
buildTree,
|
|
4
5
|
formatSize,
|
|
@@ -10,6 +11,9 @@
|
|
|
10
11
|
type SaverValue,
|
|
11
12
|
type FileItem,
|
|
12
13
|
} from './DocumentFilePicker';
|
|
14
|
+
import Button from '../Button.svelte';
|
|
15
|
+
import FolderConfirmDelete from './_FolderConfirmDelete.svelte';
|
|
16
|
+
import { documentChanges } from '../../documents/notifications';
|
|
13
17
|
|
|
14
18
|
let {
|
|
15
19
|
mode,
|
|
@@ -18,6 +22,10 @@
|
|
|
18
22
|
onCancel,
|
|
19
23
|
close,
|
|
20
24
|
suggestedName = '',
|
|
25
|
+
selectable = 'file',
|
|
26
|
+
listFolders,
|
|
27
|
+
handle,
|
|
28
|
+
readOnlyShard,
|
|
21
29
|
}: {
|
|
22
30
|
mode: 'open' | 'save';
|
|
23
31
|
docs: DocEntry[];
|
|
@@ -25,16 +33,49 @@
|
|
|
25
33
|
onCancel: () => void;
|
|
26
34
|
close: () => void;
|
|
27
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;
|
|
28
46
|
} = $props();
|
|
29
47
|
|
|
48
|
+
type Selected =
|
|
49
|
+
| { kind: 'file'; doc: DocEntry }
|
|
50
|
+
| { kind: 'folder'; fullPath: string; name: string }
|
|
51
|
+
| null;
|
|
52
|
+
|
|
30
53
|
let shardId = $state<string | null>(null);
|
|
31
54
|
let prefix = $state('');
|
|
32
|
-
let
|
|
33
|
-
let filename = $state(suggestedName);
|
|
55
|
+
let selected = $state<Selected>(null);
|
|
56
|
+
let filename = $state(untrack(() => suggestedName));
|
|
34
57
|
let activeIdx = $state(0);
|
|
35
58
|
let listEl = $state<HTMLElement | undefined>(undefined);
|
|
36
59
|
|
|
37
|
-
|
|
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));
|
|
38
79
|
const crumbs = $derived(breadcrumbSegments(shardId, prefix));
|
|
39
80
|
|
|
40
81
|
$effect(() => {
|
|
@@ -42,10 +83,33 @@
|
|
|
42
83
|
activeIdx = Math.min(activeIdx, items.length - 1);
|
|
43
84
|
});
|
|
44
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
|
+
|
|
45
109
|
function navigateShard(id: string) {
|
|
46
110
|
shardId = id;
|
|
47
111
|
prefix = '';
|
|
48
|
-
|
|
112
|
+
selected = null;
|
|
49
113
|
filename = '';
|
|
50
114
|
activeIdx = 0;
|
|
51
115
|
}
|
|
@@ -55,18 +119,22 @@
|
|
|
55
119
|
navigateShard(p);
|
|
56
120
|
} else {
|
|
57
121
|
prefix = p;
|
|
58
|
-
|
|
122
|
+
selected = null;
|
|
59
123
|
filename = '';
|
|
60
124
|
activeIdx = 0;
|
|
61
125
|
}
|
|
62
126
|
}
|
|
63
127
|
|
|
64
|
-
function
|
|
128
|
+
function selectItem(item: FileItem) {
|
|
65
129
|
if (item.kind === 'folder') {
|
|
66
|
-
|
|
130
|
+
if (selectable !== 'file') {
|
|
131
|
+
selected = { kind: 'folder', fullPath: item.fullPath, name: item.name };
|
|
132
|
+
} else {
|
|
133
|
+
navigatePrefix(item.fullPath);
|
|
134
|
+
}
|
|
67
135
|
} else {
|
|
68
136
|
if (mode === 'open') {
|
|
69
|
-
|
|
137
|
+
selected = { kind: 'file', doc: item.doc };
|
|
70
138
|
}
|
|
71
139
|
if (mode === 'save') {
|
|
72
140
|
filename = item.name;
|
|
@@ -75,8 +143,12 @@
|
|
|
75
143
|
}
|
|
76
144
|
|
|
77
145
|
function commit() {
|
|
78
|
-
if (mode === 'open' &&
|
|
79
|
-
|
|
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
|
+
}
|
|
80
152
|
close();
|
|
81
153
|
} else if (mode === 'save' && filename.trim() && shardId) {
|
|
82
154
|
const p = prefix ? `${shardId}/${prefix}/${filename}` : `${shardId}/${filename}`;
|
|
@@ -91,10 +163,150 @@
|
|
|
91
163
|
}
|
|
92
164
|
|
|
93
165
|
function canCommit(): boolean {
|
|
94
|
-
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
|
+
}
|
|
95
172
|
return filename.trim().length > 0 && shardId !== null;
|
|
96
173
|
}
|
|
97
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
|
+
|
|
98
310
|
function onKey(e: KeyboardEvent) {
|
|
99
311
|
if (e.target instanceof HTMLInputElement) return;
|
|
100
312
|
switch (e.key) {
|
|
@@ -109,13 +321,30 @@
|
|
|
109
321
|
case 'Enter':
|
|
110
322
|
e.preventDefault();
|
|
111
323
|
if (activeIdx >= 0 && activeIdx < items.length) {
|
|
112
|
-
|
|
324
|
+
selectItem(items[activeIdx]);
|
|
113
325
|
}
|
|
114
326
|
break;
|
|
115
327
|
case 'Escape':
|
|
116
328
|
e.preventDefault();
|
|
117
329
|
cancel();
|
|
118
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;
|
|
119
348
|
case 'Backspace':
|
|
120
349
|
if (prefix) {
|
|
121
350
|
e.preventDefault();
|
|
@@ -136,10 +365,21 @@
|
|
|
136
365
|
if (item.kind === 'folder') {
|
|
137
366
|
navigatePrefix(item.fullPath);
|
|
138
367
|
} else if (mode === 'open') {
|
|
139
|
-
onCommit({ shardId: item.doc.shardId, path: item.doc.path });
|
|
368
|
+
onCommit({ shardId: item.doc.shardId, path: item.doc.path, kind: 'file' });
|
|
140
369
|
close();
|
|
141
370
|
}
|
|
142
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
|
+
});
|
|
143
383
|
</script>
|
|
144
384
|
|
|
145
385
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -160,7 +400,7 @@
|
|
|
160
400
|
onclick={() => {
|
|
161
401
|
shardId = seg.targetShard;
|
|
162
402
|
prefix = seg.targetPrefix;
|
|
163
|
-
|
|
403
|
+
selected = null;
|
|
164
404
|
filename = '';
|
|
165
405
|
activeIdx = 0;
|
|
166
406
|
}}
|
|
@@ -168,15 +408,100 @@
|
|
|
168
408
|
{/each}
|
|
169
409
|
</nav>
|
|
170
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
|
+
|
|
171
454
|
<div class="sh3-doc-browser__list" bind:this={listEl}>
|
|
172
|
-
{#if
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
/>
|
|
175
468
|
</div>
|
|
176
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
|
+
|
|
177
494
|
{#each items as item, i}
|
|
178
495
|
{@const isActive = i === activeIdx}
|
|
179
|
-
{@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
|
+
)}
|
|
180
505
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
181
506
|
<div
|
|
182
507
|
class="sh3-doc-browser__item"
|
|
@@ -186,22 +511,52 @@
|
|
|
186
511
|
role="option"
|
|
187
512
|
tabindex="-1"
|
|
188
513
|
aria-selected={isSelected}
|
|
189
|
-
onclick={() =>
|
|
190
|
-
onkeydown={(e) => { if (e.key === 'Enter')
|
|
514
|
+
onclick={() => selectItem(item)}
|
|
515
|
+
onkeydown={(e) => { if (e.key === 'Enter') selectItem(item); }}
|
|
191
516
|
ondblclick={() => onDblClick(item)}
|
|
192
517
|
onmouseenter={() => activeIdx = i}
|
|
193
518
|
>
|
|
194
519
|
{#if item.kind === 'folder'}
|
|
195
520
|
<span class="sh3-doc-browser__icon sh3-doc-browser__icon--folder" aria-hidden="true">📁</span>
|
|
196
|
-
|
|
197
|
-
|
|
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}
|
|
198
538
|
{:else}
|
|
199
539
|
<span class="sh3-doc-browser__icon" aria-hidden="true">{iconForFile(item.name)}</span>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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}
|
|
205
560
|
{/if}
|
|
206
561
|
</div>
|
|
207
562
|
{/each}
|
|
@@ -236,13 +591,9 @@
|
|
|
236
591
|
|
|
237
592
|
<style>
|
|
238
593
|
.sh3-doc-browser {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
box-shadow: var(--sh3-shadow-lg);
|
|
243
|
-
width: 420px;
|
|
244
|
-
max-height: 480px;
|
|
245
|
-
display: flex; flex-direction: column;
|
|
594
|
+
display: flex;
|
|
595
|
+
flex-direction: column;
|
|
596
|
+
max-height: 85vh;
|
|
246
597
|
overflow: hidden;
|
|
247
598
|
color: var(--sh3-fg);
|
|
248
599
|
font-size: 0.8125rem;
|
|
@@ -275,11 +626,33 @@
|
|
|
275
626
|
.sh3-doc-browser__crumb:hover { background: var(--sh3-bg); color: var(--sh3-fg); }
|
|
276
627
|
.sh3-doc-browser__crumb--last { color: var(--sh3-fg); font-weight: 600; }
|
|
277
628
|
.sh3-doc-browser__crumb-sep { color: var(--sh3-fg-subtle); font-size: 0.75rem; flex-shrink: 0; }
|
|
278
|
-
.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
|
+
}
|
|
279
651
|
.sh3-doc-browser__item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; cursor: pointer; }
|
|
280
652
|
.sh3-doc-browser__item--active { background: var(--sh3-bg); }
|
|
281
653
|
.sh3-doc-browser__item--selected { background: var(--sh3-accent); color: var(--sh3-fg-on-accent); }
|
|
282
654
|
.sh3-doc-browser__item--active.sh3-doc-browser__item--selected { background: var(--sh3-accent); }
|
|
655
|
+
.sh3-doc-browser__item--editing { cursor: default; }
|
|
283
656
|
.sh3-doc-browser__icon { flex-shrink: 0; width: 18px; text-align: center; font-size: 0.875rem; }
|
|
284
657
|
.sh3-doc-browser__icon--folder { font-size: 0.75rem; }
|
|
285
658
|
.sh3-doc-browser__name {
|
|
@@ -288,6 +661,17 @@
|
|
|
288
661
|
font-family: var(--sh3-font-mono); font-size: 0.75rem;
|
|
289
662
|
}
|
|
290
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
|
+
}
|
|
291
675
|
.sh3-doc-browser__meta {
|
|
292
676
|
display: flex; align-items: center; gap: 10px;
|
|
293
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 {};
|