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.
Files changed (103) hide show
  1. package/dist/BrandSlot.svelte +2 -2
  2. package/dist/actions/ctx-actions.svelte.test.js +2 -2
  3. package/dist/artifact.d.ts +2 -0
  4. package/dist/boot/satellitePayload.d.ts +2 -0
  5. package/dist/boot/satellitePayload.test.js +19 -0
  6. package/dist/build.d.ts +7 -1
  7. package/dist/build.js +22 -3
  8. package/dist/build.test.js +27 -1
  9. package/dist/createShell.js +34 -9
  10. package/dist/documents/backends.d.ts +12 -0
  11. package/dist/documents/backends.js +230 -3
  12. package/dist/documents/backends.test.js +147 -1
  13. package/dist/documents/browse.d.ts +20 -0
  14. package/dist/documents/browse.js +35 -0
  15. package/dist/documents/browse.test.js +125 -0
  16. package/dist/documents/config.d.ts +2 -4
  17. package/dist/documents/config.js +3 -7
  18. package/dist/documents/handle.js +40 -0
  19. package/dist/documents/handle.test.js +88 -1
  20. package/dist/documents/http-backend.d.ts +11 -0
  21. package/dist/documents/http-backend.js +86 -0
  22. package/dist/documents/http-backend.test.js +117 -1
  23. package/dist/documents/index.d.ts +1 -1
  24. package/dist/documents/index.js +1 -1
  25. package/dist/documents/picker-api.test.js +2 -2
  26. package/dist/documents/types.d.ts +87 -14
  27. package/dist/documents/types.js +4 -0
  28. package/dist/host-entry.d.ts +1 -1
  29. package/dist/host-entry.js +1 -1
  30. package/dist/host.d.ts +1 -1
  31. package/dist/host.js +1 -1
  32. package/dist/layout/slotHostPool.svelte.js +2 -2
  33. package/dist/overlays/FloatFrame.svelte +1 -0
  34. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  35. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  36. package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
  37. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  38. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  39. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  40. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  41. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  42. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  43. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  44. package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
  45. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  46. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  47. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  48. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  49. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  50. package/dist/projects/session-state.svelte.d.ts +3 -0
  51. package/dist/projects/session-state.svelte.js +25 -0
  52. package/dist/projects/session-state.test.js +43 -2
  53. package/dist/projects-shard/ProjectsSection.svelte +14 -18
  54. package/dist/runtime/runVerb-shell.test.js +2 -2
  55. package/dist/runtime/runVerb.test.js +2 -2
  56. package/dist/sh3Api/headless.js +10 -0
  57. package/dist/sh3core-shard/appActions.js +5 -2
  58. package/dist/shards/activate-browse.test.js +2 -2
  59. package/dist/shards/activate-contributions.test.js +2 -2
  60. package/dist/shards/activate-error-isolation.test.js +3 -3
  61. package/dist/shards/activate-on-key-revoked.test.js +2 -2
  62. package/dist/shards/activate-runtime.test.js +2 -2
  63. package/dist/shards/activate.svelte.js +5 -5
  64. package/dist/shards/ctx-fetch.test.js +4 -4
  65. package/dist/shell-shard/Terminal.svelte +4 -1
  66. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  67. package/dist/shell-shard/dispatch.d.ts +2 -0
  68. package/dist/shell-shard/dispatch.js +2 -0
  69. package/dist/shell-shard/manifest.js +7 -1
  70. package/dist/shell-shard/shellShard.svelte.js +1 -1
  71. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  72. package/dist/shell-shard/verbs/cat.js +35 -0
  73. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  74. package/dist/shell-shard/verbs/cat.test.js +49 -0
  75. package/dist/shell-shard/verbs/index.js +12 -0
  76. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  77. package/dist/shell-shard/verbs/ls.js +48 -0
  78. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  79. package/dist/shell-shard/verbs/ls.test.js +64 -0
  80. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  81. package/dist/shell-shard/verbs/mkdir.js +30 -0
  82. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  83. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  84. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  85. package/dist/shell-shard/verbs/mv.js +33 -0
  86. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  87. package/dist/shell-shard/verbs/mv.test.js +55 -0
  88. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  89. package/dist/shell-shard/verbs/rm.js +28 -0
  90. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  91. package/dist/shell-shard/verbs/rm.test.js +47 -0
  92. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  93. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  94. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  95. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  96. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  97. package/dist/shell-shard/verbs/xfer.js +87 -0
  98. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  99. package/dist/shell-shard/verbs/xfer.test.js +107 -0
  100. package/dist/verbs/types.d.ts +18 -0
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. 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 selectedFile = $state<DocEntry | null>(null);
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
- 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));
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
- selectedFile = null;
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
- selectedFile = null;
122
+ selected = null;
60
123
  filename = '';
61
124
  activeIdx = 0;
62
125
  }
63
126
  }
64
127
 
65
- function selectFile(item: FileItem) {
128
+ function selectItem(item: FileItem) {
66
129
  if (item.kind === 'folder') {
67
- navigatePrefix(item.fullPath);
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
- selectedFile = item.doc;
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' && selectedFile) {
80
- 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
+ }
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') 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
+ }
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
- selectFile(items[activeIdx]);
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
- selectedFile = null;
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 items.length === 0}
174
- <div class="sh3-doc-browser__empty">
175
- {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
+ />
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 = 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
+ )}
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={() => selectFile(item)}
191
- onkeydown={(e) => { if (e.key === 'Enter') selectFile(item); }}
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
- <span class="sh3-doc-browser__name">{item.name}</span>
198
- <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}
199
538
  {:else}
200
539
  <span class="sh3-doc-browser__icon" aria-hidden="true">{iconForFile(item.name)}</span>
201
- <span class="sh3-doc-browser__name">{item.name}</span>
202
- <span class="sh3-doc-browser__meta">
203
- {formatSize(item.doc.size)}
204
- <span class="sh3-doc-browser__date">{formatDate(item.doc.lastModified)}</span>
205
- </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}
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-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
+ }
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>;