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.
Files changed (123) hide show
  1. package/dist/app/admin/AuthSettingsView.svelte +3 -9
  2. package/dist/app/admin/MountsView.svelte +276 -0
  3. package/dist/app/admin/MountsView.svelte.d.ts +3 -0
  4. package/dist/app/admin/SystemView.svelte +6 -6
  5. package/dist/app/admin/UsersView.svelte +103 -7
  6. package/dist/app/admin/adminApp.js +1 -0
  7. package/dist/app/admin/adminShard.svelte.js +10 -0
  8. package/dist/apps/lifecycle.js +1 -0
  9. package/dist/apps/types.d.ts +7 -0
  10. package/dist/assets/iconIds.generated.d.ts +1 -1
  11. package/dist/assets/iconIds.generated.js +1 -0
  12. package/dist/assets/icons.svg +5 -0
  13. package/dist/auth/admin-users.svelte.js +2 -1
  14. package/dist/auth/auth.svelte.d.ts +4 -5
  15. package/dist/auth/auth.svelte.js +5 -6
  16. package/dist/auth/types.d.ts +0 -2
  17. package/dist/chrome/CompactChrome.svelte +25 -6
  18. package/dist/chrome/FloatsSheet.svelte +7 -32
  19. package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
  20. package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
  21. package/dist/chrome/MenuSheet.svelte +154 -148
  22. package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
  23. package/dist/chrome/MenuSheet.svelte.test.js +24 -12
  24. package/dist/createShell.js +32 -21
  25. package/dist/createShell.remoteAuth.test.js +9 -3
  26. package/dist/documents/backends.d.ts +12 -0
  27. package/dist/documents/backends.js +230 -3
  28. package/dist/documents/backends.test.js +147 -1
  29. package/dist/documents/browse.d.ts +18 -1
  30. package/dist/documents/browse.js +40 -7
  31. package/dist/documents/browse.test.js +35 -35
  32. package/dist/documents/config.d.ts +6 -0
  33. package/dist/documents/config.js +18 -1
  34. package/dist/documents/handle.js +65 -17
  35. package/dist/documents/handle.test.js +88 -1
  36. package/dist/documents/http-backend.d.ts +6 -0
  37. package/dist/documents/http-backend.js +71 -2
  38. package/dist/documents/http-backend.test.js +51 -1
  39. package/dist/documents/index.d.ts +2 -2
  40. package/dist/documents/index.js +1 -1
  41. package/dist/documents/picker-api.d.ts +4 -2
  42. package/dist/documents/picker-api.test.d.ts +1 -1
  43. package/dist/documents/picker-api.test.js +89 -59
  44. package/dist/documents/picker-primitive.d.ts +4 -0
  45. package/dist/documents/picker-primitive.js +27 -29
  46. package/dist/documents/types.d.ts +93 -19
  47. package/dist/documents/types.js +6 -0
  48. package/dist/layout/presets.test.js +4 -4
  49. package/dist/layout/types.d.ts +1 -1
  50. package/dist/layouts-shard/LayoutsSection.svelte +3 -16
  51. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  52. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  53. package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
  54. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  55. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  56. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  57. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  58. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  59. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  60. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  61. package/dist/primitives/widgets/PickerList.svelte +1 -0
  62. package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
  63. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  64. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  65. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  66. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  67. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  68. package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
  69. package/dist/projects-shard/ProjectManage.svelte +197 -28
  70. package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
  71. package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
  72. package/dist/projects-shard/ProjectsSection.svelte +3 -16
  73. package/dist/projects-shard/projectsApi.js +2 -1
  74. package/dist/registry/permission-descriptions.js +4 -0
  75. package/dist/server-shard/types.d.ts +21 -0
  76. package/dist/sh3Api/headless.js +10 -0
  77. package/dist/sh3core-shard/HomeSection.svelte +107 -0
  78. package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
  79. package/dist/sh3core-shard/Sh3Home.svelte +9 -23
  80. package/dist/shards/activate.svelte.d.ts +4 -0
  81. package/dist/shards/activate.svelte.js +11 -3
  82. package/dist/shards/types.d.ts +7 -0
  83. package/dist/shell-shard/Terminal.svelte +4 -1
  84. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  85. package/dist/shell-shard/dispatch.d.ts +2 -0
  86. package/dist/shell-shard/dispatch.js +2 -0
  87. package/dist/shell-shard/manifest.js +7 -1
  88. package/dist/shell-shard/shellShard.svelte.js +1 -1
  89. package/dist/shell-shard/tenant-fs-client.js +2 -1
  90. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  91. package/dist/shell-shard/verbs/cat.js +35 -0
  92. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  93. package/dist/shell-shard/verbs/cat.test.js +49 -0
  94. package/dist/shell-shard/verbs/index.js +12 -0
  95. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  96. package/dist/shell-shard/verbs/ls.js +48 -0
  97. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  98. package/dist/shell-shard/verbs/ls.test.js +64 -0
  99. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  100. package/dist/shell-shard/verbs/mkdir.js +30 -0
  101. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  102. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  103. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  104. package/dist/shell-shard/verbs/mv.js +33 -0
  105. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  106. package/dist/shell-shard/verbs/mv.test.js +55 -0
  107. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  108. package/dist/shell-shard/verbs/rm.js +28 -0
  109. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  110. package/dist/shell-shard/verbs/rm.test.js +47 -0
  111. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  112. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  113. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  114. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  115. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  116. package/dist/shell-shard/verbs/xfer.js +101 -0
  117. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  118. package/dist/shell-shard/verbs/xfer.test.js +96 -0
  119. package/dist/transport/apiFetch.js +12 -5
  120. package/dist/verbs/types.d.ts +18 -0
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. 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 selectedFile = $state<DocEntry | null>(null);
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
- const items = $derived(buildTree(docs, shardId, prefix));
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
- selectedFile = null;
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
- selectedFile = null;
122
+ selected = null;
59
123
  filename = '';
60
124
  activeIdx = 0;
61
125
  }
62
126
  }
63
127
 
64
- function selectFile(item: FileItem) {
128
+ function selectItem(item: FileItem) {
65
129
  if (item.kind === 'folder') {
66
- navigatePrefix(item.fullPath);
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
- selectedFile = item.doc;
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' && selectedFile) {
79
- onCommit({ shardId: selectedFile.shardId, path: selectedFile.path });
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') return selectedFile !== null;
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
- selectFile(items[activeIdx]);
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
- selectedFile = null;
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 items.length === 0}
173
- <div class="sh3-doc-browser__empty">
174
- {shardId === null ? 'No shards available.' : prefix ? 'Empty directory.' : 'No documents in this shard.'}
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 = item.kind === 'file' && mode === 'open' && selectedFile?.path === item.doc.path && selectedFile?.shardId === item.doc.shardId}
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={() => selectFile(item)}
190
- onkeydown={(e) => { if (e.key === 'Enter') selectFile(item); }}
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
- <span class="sh3-doc-browser__name">{item.name}</span>
197
- <span class="sh3-doc-browser__meta"></span>
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
- <span class="sh3-doc-browser__name">{item.name}</span>
201
- <span class="sh3-doc-browser__meta">
202
- {formatSize(item.doc.size)}
203
- <span class="sh3-doc-browser__date">{formatDate(item.doc.lastModified)}</span>
204
- </span>
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
- background: var(--sh3-bg-elevated);
240
- border: 1px solid var(--sh3-border-strong);
241
- border-radius: var(--sh3-widget-radius);
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-browser__list { flex: 1; overflow-y: auto; padding: 4px 0; min-height: 0; }
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>;