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
@@ -34,6 +34,8 @@ export declare const PERMISSION_DOCUMENTS_READ = "documents:read";
34
34
  * `browse`.
35
35
  */
36
36
  export declare const PERMISSION_DOCUMENTS_WRITE = "documents:write";
37
+ /** Permission to manage document mount points (admin-only). */
38
+ export declare const PERMISSION_DOCUMENTS_MOUNT = "documents:mount";
37
39
  /**
38
40
  * Format hint for document content. Determines whether reads return a string
39
41
  * (`text`) or an `ArrayBuffer` (`binary`).
@@ -71,26 +73,37 @@ export interface DocumentMeta {
71
73
  deleted?: boolean;
72
74
  }
73
75
  /** Change notification payload delivered to watch callbacks. */
74
- export interface DocumentChange {
75
- type: 'create' | 'update' | 'delete' | 'rename';
76
- /**
77
- * For 'create' / 'update' / 'delete', the affected document path.
78
- * For 'rename', the new path the document now lives at.
79
- */
76
+ export type DocumentChange = {
77
+ type: 'create' | 'update' | 'delete';
80
78
  path: string;
81
- /**
82
- * Populated only when type === 'rename'. The path the document
83
- * used to live at before the rename.
84
- */
85
- oldPath?: string;
86
79
  tenantId: string;
87
80
  shardId: string;
88
- }
89
- /** Type guard: narrows a DocumentChange to the rename variant. */
90
- export declare function isRename(change: DocumentChange): change is DocumentChange & {
81
+ } | {
91
82
  type: 'rename';
83
+ path: string;
84
+ oldPath: string;
85
+ tenantId: string;
86
+ shardId: string;
87
+ } | {
88
+ type: 'folder-create' | 'folder-delete';
89
+ path: string;
90
+ tenantId: string;
91
+ shardId: string;
92
+ } | {
93
+ type: 'folder-rename';
94
+ path: string;
92
95
  oldPath: string;
96
+ tenantId: string;
97
+ shardId: string;
93
98
  };
99
+ /** Type guard: narrows a DocumentChange to the rename variant. */
100
+ export declare function isRename(change: DocumentChange): change is Extract<DocumentChange, {
101
+ type: 'rename';
102
+ }>;
103
+ /** Type guard: narrows a DocumentChange to the folder-rename variant. */
104
+ export declare function isFolderRename(change: DocumentChange): change is Extract<DocumentChange, {
105
+ type: 'folder-rename';
106
+ }>;
94
107
  import type { DocStatus } from './sync-types';
95
108
  /**
96
109
  * File-oriented backend for the document zone.
@@ -118,6 +131,31 @@ export interface DocumentBackend {
118
131
  * as folder semantics.
119
132
  */
120
133
  rename(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
134
+ /**
135
+ * Create an empty folder. No-op if the folder already exists.
136
+ * Throws if a document occupies the path.
137
+ */
138
+ mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
139
+ /**
140
+ * Remove a folder. Throws if non-empty and `recursive` is false.
141
+ * When `recursive: true`, atomically removes the folder and all
142
+ * descendant documents and folders.
143
+ */
144
+ rmdir(tenantId: string, shardId: string, path: string, opts: {
145
+ recursive: boolean;
146
+ }): Promise<void>;
147
+ /**
148
+ * Rename a folder atomically. Rewrites all descendant document paths
149
+ * to use the new prefix. Throws if `oldPath` does not exist as a
150
+ * folder, or if `newPath` already exists.
151
+ */
152
+ renameFolder(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
153
+ /**
154
+ * List immediate folder children of `prefix`. Returns folder names
155
+ * (not full paths). Empty `prefix` lists folders directly under the
156
+ * shard root.
157
+ */
158
+ listFolders(tenantId: string, shardId: string, prefix: string): Promise<string[]>;
121
159
  /** List all documents stored for this tenant + shard combination. */
122
160
  list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
123
161
  /** Return true if the document at `path` exists. */
@@ -158,23 +196,59 @@ export interface DocumentBackend {
158
196
  /**
159
197
  * Shard-facing document handle returned by `ctx.documents()`. Binds
160
198
  * the tenant, shard, and backend so shard code only deals in paths.
199
+ *
200
+ * All read/write/list/delete/rename methods accept an optional scope
201
+ * override for cross-scope document transfers (e.g. moving documents
202
+ * between a personal tenant and a project). When omitted, the handle's
203
+ * bound tenant is used.
161
204
  */
205
+ /** Optional scope override for cross-tenant operations. */
206
+ export interface ScopeOption {
207
+ /** Override the handle's bound tenant for a single operation. */
208
+ scope?: string;
209
+ }
162
210
  export interface DocumentHandle {
163
211
  /** List documents matching the handle's extensions filter. */
164
- list(): Promise<DocumentMeta[]>;
212
+ list(opts?: ScopeOption): Promise<DocumentMeta[]>;
165
213
  /** Read a document by path. Returns null if not found. */
166
- read(path: string): Promise<string | null>;
214
+ read(path: string, opts?: ScopeOption): Promise<string | null>;
167
215
  /** Write (create or overwrite) a document. Explicit save. */
168
- write(path: string, content: string): Promise<void>;
216
+ write(path: string, content: string, opts?: ScopeOption): Promise<void>;
169
217
  /** Delete a document. */
170
- delete(path: string): Promise<void>;
218
+ delete(path: string, opts?: ScopeOption): Promise<void>;
171
219
  /**
172
220
  * Rename a document. Throws if there is an active autosave controller
173
221
  * for oldPath (caller must flush and dispose first). Throws if newPath
174
222
  * already exists or if oldPath does not. Subject to the handle's
175
223
  * extensions filter — newPath must satisfy the filter.
176
224
  */
177
- rename(oldPath: string, newPath: string): Promise<void>;
225
+ rename(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
226
+ /**
227
+ * Create an empty folder at `path`. No-op if the folder already
228
+ * exists. Throws if a document occupies the path.
229
+ */
230
+ mkdir(path: string, opts?: ScopeOption): Promise<void>;
231
+ /**
232
+ * Remove a folder. Throws if non-empty and `opts.recursive` is not
233
+ * true. When `opts.recursive` is true, atomically removes the folder
234
+ * and all descendants. Throws if any active autosave controller's
235
+ * path falls inside the folder.
236
+ */
237
+ rmdir(path: string, opts?: {
238
+ recursive?: boolean;
239
+ } & ScopeOption): Promise<void>;
240
+ /**
241
+ * Rename a folder. Atomically rewrites all descendant document paths.
242
+ * Throws if any active autosave controller's path falls inside the
243
+ * folder (caller must flush and dispose first). Throws if `newPath`
244
+ * already exists or if `oldPath` does not.
245
+ */
246
+ renameFolder(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
247
+ /**
248
+ * List immediate folder children of `prefix`. Empty `prefix` (or
249
+ * omitted) lists folders directly under the shard root.
250
+ */
251
+ listFolders(prefix?: string, opts?: ScopeOption): Promise<string[]>;
178
252
  /** Check existence without reading content. */
179
253
  exists(path: string): Promise<boolean>;
180
254
  /** Fetch sync-state metadata for a path. Null if the doc does not exist. */
@@ -45,7 +45,13 @@ export const PERMISSION_DOCUMENTS_READ = 'documents:read';
45
45
  * `browse`.
46
46
  */
47
47
  export const PERMISSION_DOCUMENTS_WRITE = 'documents:write';
48
+ /** Permission to manage document mount points (admin-only). */
49
+ export const PERMISSION_DOCUMENTS_MOUNT = 'documents:mount';
48
50
  /** Type guard: narrows a DocumentChange to the rename variant. */
49
51
  export function isRename(change) {
50
52
  return change.type === 'rename';
51
53
  }
54
+ /** Type guard: narrows a DocumentChange to the folder-rename variant. */
55
+ export function isFolderRename(change) {
56
+ return change.type === 'folder-rename';
57
+ }
@@ -25,7 +25,7 @@ describe('normalizeInitialLayout', () => {
25
25
  });
26
26
  it('canonicalizes a preset list, using tree as default and preserving variants', () => {
27
27
  const authorTree = { docked: leafNode, floats: [] };
28
- const companionTree = {
28
+ const compactTree = {
29
29
  docked: { type: 'slot', slotId: 's2', viewId: 'v2' },
30
30
  floats: [],
31
31
  };
@@ -33,7 +33,7 @@ describe('normalizeInitialLayout', () => {
33
33
  {
34
34
  name: 'author',
35
35
  tree: authorTree,
36
- variants: { companion: companionTree },
36
+ variants: { compact: compactTree },
37
37
  },
38
38
  ];
39
39
  const result = normalizeInitialLayout(presets);
@@ -42,7 +42,7 @@ describe('normalizeInitialLayout', () => {
42
42
  name: 'author',
43
43
  variants: {
44
44
  default: authorTree,
45
- companion: companionTree,
45
+ compact: compactTree,
46
46
  },
47
47
  },
48
48
  ]);
@@ -54,7 +54,7 @@ describe('normalizeInitialLayout', () => {
54
54
  expect(result).toEqual([{ name: 'x', variants: { default: tree } }]);
55
55
  });
56
56
  it('throws if a preset has neither tree nor variants.default', () => {
57
- const bad = [{ name: 'broken', variants: { companion: { docked: leafNode, floats: [] } } }];
57
+ const bad = [{ name: 'broken', variants: { compact: { docked: leafNode, floats: [] } } }];
58
58
  expect(() => normalizeInitialLayout(bad)).toThrow(/must provide either 'tree' or 'variants.default'/);
59
59
  });
60
60
  it('when a preset has both tree and variants.default, variants.default wins', () => {
@@ -176,7 +176,7 @@ export interface LayoutTree {
176
176
  * manifest; users switch between them at runtime. The ergonomic `tree`
177
177
  * field is shorthand for `variants.default`; the normalizer canonicalizes
178
178
  * every preset into a variants-only shape on load. v1 always uses the
179
- * `default` variant; other variant keys (e.g. `companion`) are reserved
179
+ * `default` variant; other variant keys (e.g. `compact`) are reserved
180
180
  * for the rescoped DF10 selection policy and are persisted but inert.
181
181
  */
182
182
  export interface LayoutPreset {
@@ -11,6 +11,7 @@
11
11
  import { toastManager } from '../overlays/toast';
12
12
  import { sh3 } from '../sh3Runtime.svelte';
13
13
  import { makeSelectionApi } from '../actions/selection.svelte';
14
+ import HomeSection from '../sh3core-shard/HomeSection.svelte';
14
15
  import iconsUrl from '../assets/icons.svg';
15
16
 
16
17
  const layouts = $derived(getLayouts());
@@ -41,8 +42,7 @@
41
42
  </script>
42
43
 
43
44
  {#if layouts.length > 0}
44
- <section class="saved-layouts-section">
45
- <h2 class="saved-layouts-heading">Saved Layouts</h2>
45
+ <HomeSection title="Saved Layouts" persistKey="layouts">
46
46
  <div class="saved-layouts-grid">
47
47
  {#each layouts as layout (layout.id)}
48
48
  <button
@@ -64,23 +64,10 @@
64
64
  </button>
65
65
  {/each}
66
66
  </div>
67
- </section>
67
+ </HomeSection>
68
68
  {/if}
69
69
 
70
70
  <style>
71
- .saved-layouts-section {
72
- width: 100%;
73
- max-width: 720px;
74
- margin-bottom: 28px;
75
- }
76
- .saved-layouts-heading {
77
- font-size: 13px;
78
- font-weight: 600;
79
- text-transform: uppercase;
80
- letter-spacing: 0.06em;
81
- color: var(--sh3-fg-subtle);
82
- margin: 0 0 12px;
83
- }
84
71
  .saved-layouts-grid {
85
72
  display: grid;
86
73
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
@@ -2,7 +2,11 @@ import type { DocumentMeta } from '../../documents/types';
2
2
  export type DocEntry = DocumentMeta & {
3
3
  shardId: string;
4
4
  };
5
- export type OpenerValue = Pick<DocEntry, 'shardId' | 'path'> | null;
5
+ export type OpenerValue = {
6
+ shardId: string;
7
+ path: string;
8
+ kind: 'file' | 'folder';
9
+ } | null;
6
10
  export type SaverValue = string | null;
7
11
  export type FileItem = {
8
12
  kind: 'folder';
@@ -13,7 +17,7 @@ export type FileItem = {
13
17
  name: string;
14
18
  doc: DocEntry;
15
19
  };
16
- export declare function buildTree(docs: DocEntry[], shardId: string | null, prefix: string): FileItem[];
20
+ export declare function buildTree(docs: DocEntry[], folders: string[], shardId: string | null, prefix: string): FileItem[];
17
21
  export declare function formatSize(bytes: number): string;
18
22
  export declare function formatDate(epochMs: number): string;
19
23
  export declare function iconForFile(name: string): string;
@@ -1,10 +1,10 @@
1
- export function buildTree(docs, shardId, prefix) {
1
+ export function buildTree(docs, folders, shardId, prefix) {
2
2
  if (shardId === null) {
3
3
  const shards = [...new Set(docs.map((d) => d.shardId))].sort();
4
4
  return shards.map((s) => ({ kind: 'folder', name: s, fullPath: s }));
5
5
  }
6
6
  const shardDocs = docs.filter((d) => d.shardId === shardId);
7
- const folders = new Map();
7
+ const folderMap = new Map();
8
8
  const files = [];
9
9
  const normPrefix = prefix ? prefix + '/' : '';
10
10
  const plen = normPrefix.length;
@@ -16,14 +16,21 @@ export function buildTree(docs, shardId, prefix) {
16
16
  if (slash >= 0) {
17
17
  const name = relative.slice(0, slash);
18
18
  const full = prefix ? `${prefix}/${name}` : name;
19
- if (!folders.has(name))
20
- folders.set(name, full);
19
+ if (!folderMap.has(name))
20
+ folderMap.set(name, full);
21
21
  }
22
22
  else {
23
23
  files.push({ kind: 'file', name: relative, doc });
24
24
  }
25
25
  }
26
- const folderItems = [...folders.entries()]
26
+ // Merge explicit empty folders (immediate children of prefix)
27
+ for (const name of folders) {
28
+ if (folderMap.has(name))
29
+ continue;
30
+ const full = prefix ? `${prefix}/${name}` : name;
31
+ folderMap.set(name, full);
32
+ }
33
+ const folderItems = [...folderMap.entries()]
27
34
  .map(([name, fullPath]) => ({ kind: 'folder', name, fullPath }))
28
35
  .sort((a, b) => a.name.localeCompare(b.name));
29
36
  const fileItems = files.sort((a, b) => a.name.localeCompare(b.name));
@@ -6,24 +6,40 @@
6
6
  import type { DocEntry, OpenerValue, SaverValue } from './DocumentFilePicker';
7
7
 
8
8
  type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
9
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
10
+ type HandleFn = {
11
+ mkdir: (shardId: string, path: string) => Promise<void>;
12
+ rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
13
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
15
+ delete: (shardId: string, path: string) => Promise<void>;
16
+ };
9
17
 
10
18
  let {
11
19
  mode,
12
20
  value = $bindable<OpenerValue | SaverValue>(null),
13
21
  listDocuments,
22
+ listFolders,
23
+ handle,
24
+ readOnlyShard,
14
25
  disabled = false,
15
26
  invalid = false,
16
27
  size = 'md',
17
28
  buttonLabel = 'Choose…',
29
+ selectable = 'file',
18
30
  onchange,
19
31
  }: {
20
32
  mode: 'open' | 'save';
21
33
  value?: OpenerValue | SaverValue;
22
34
  listDocuments: DocListFn;
35
+ listFolders?: FolderListFn;
36
+ handle?: HandleFn;
37
+ readOnlyShard?: (shardId: string) => boolean;
23
38
  disabled?: boolean;
24
39
  invalid?: boolean;
25
40
  size?: 'sm' | 'md';
26
41
  buttonLabel?: string;
42
+ selectable?: 'file' | 'folder' | 'both';
27
43
  } & CommitOnlyEvents<OpenerValue | SaverValue> = $props();
28
44
 
29
45
  let trigger = $state<HTMLButtonElement | undefined>(undefined);
@@ -33,7 +49,9 @@
33
49
  value
34
50
  ? typeof value === 'string'
35
51
  ? value
36
- : `${value.shardId}/${value.path}`
52
+ : value.kind === 'folder'
53
+ ? `${value.shardId}/${value.path}/`
54
+ : `${value.shardId}/${value.path}`
37
55
  : null,
38
56
  );
39
57
 
@@ -58,21 +76,25 @@
58
76
  return;
59
77
  }
60
78
 
61
- const popupHandle = sh3.popup.show(
79
+ const modalHandle = sh3.modal.open(
62
80
  DocumentBrowser,
63
- { anchor: trigger },
64
81
  {
65
82
  mode,
66
83
  docs,
84
+ selectable,
85
+ listFolders,
86
+ handle,
87
+ readOnlyShard,
67
88
  onCommit: (result: OpenerValue | SaverValue) => {
68
89
  handleCommit(result);
69
90
  },
70
91
  onCancel: () => {},
71
92
  },
93
+ { dismissOnBackdrop: true, boxStyle: 'max-width: min(800px, 95vw);' },
72
94
  );
73
95
 
74
- const origClose = popupHandle.close;
75
- popupHandle.close = () => {
96
+ const origClose = modalHandle.close;
97
+ modalHandle.close = () => {
76
98
  origClose();
77
99
  onOpenClosed();
78
100
  };
@@ -4,14 +4,28 @@ import type { OpenerValue, SaverValue } from './DocumentFilePicker';
4
4
  type DocListFn = () => Promise<Array<DocumentMeta & {
5
5
  shardId: string;
6
6
  }>>;
7
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
8
+ type HandleFn = {
9
+ mkdir: (shardId: string, path: string) => Promise<void>;
10
+ rmdir: (shardId: string, path: string, opts: {
11
+ recursive: boolean;
12
+ }) => Promise<void>;
13
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
15
+ delete: (shardId: string, path: string) => Promise<void>;
16
+ };
7
17
  type $$ComponentProps = {
8
18
  mode: 'open' | 'save';
9
19
  value?: OpenerValue | SaverValue;
10
20
  listDocuments: DocListFn;
21
+ listFolders?: FolderListFn;
22
+ handle?: HandleFn;
23
+ readOnlyShard?: (shardId: string) => boolean;
11
24
  disabled?: boolean;
12
25
  invalid?: boolean;
13
26
  size?: 'sm' | 'md';
14
27
  buttonLabel?: string;
28
+ selectable?: 'file' | 'folder' | 'both';
15
29
  } & CommitOnlyEvents<OpenerValue | SaverValue>;
16
30
  declare const DocumentFilePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
17
31
  type DocumentFilePicker = ReturnType<typeof DocumentFilePicker>;
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildTree } from './DocumentFilePicker';
3
+ describe('buildTree with explicit folders', () => {
4
+ const docs = [
5
+ { shardId: 'sh1', path: 'a.md', size: 0, lastModified: 0 },
6
+ { shardId: 'sh1', path: 'sub/b.md', size: 0, lastModified: 0 },
7
+ ];
8
+ it('shard root returns shard list (folders ignored at root)', () => {
9
+ const items = buildTree(docs, [], null, '');
10
+ expect(items.map((i) => i.kind === 'folder' && i.name)).toEqual(['sh1']);
11
+ });
12
+ it('within a shard, merges implicit folders with empty explicit folders', () => {
13
+ const items = buildTree(docs, ['emptyDir'], 'sh1', '');
14
+ const folderNames = items.filter((i) => i.kind === 'folder').map((i) => i.name).sort();
15
+ expect(folderNames).toEqual(['emptyDir', 'sub']);
16
+ });
17
+ it('deduplicates explicit and implicit folders with the same name', () => {
18
+ const items = buildTree(docs, ['sub'], 'sh1', '');
19
+ const folderNames = items.filter((i) => i.kind === 'folder').map((i) => i.name);
20
+ expect(folderNames).toEqual(['sub']);
21
+ });
22
+ it('file items carry kind:"file"', () => {
23
+ const items = buildTree(docs, [], 'sh1', '');
24
+ const fileItems = items.filter((i) => i.kind === 'file');
25
+ expect(fileItems[0]).toMatchObject({ kind: 'file', name: 'a.md' });
26
+ });
27
+ });
28
+ describe('OpenerValue type accepts folder kind', () => {
29
+ it('compiles with kind:"folder"', () => {
30
+ const v = { shardId: 'sh1', path: 'sub', kind: 'folder' };
31
+ expect(v === null || v === void 0 ? void 0 : v.kind).toBe('folder');
32
+ });
33
+ });
@@ -5,10 +5,22 @@
5
5
  import type { OpenerValue } from './DocumentFilePicker';
6
6
 
7
7
  type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
8
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
9
+ type HandleFn = {
10
+ mkdir: (shardId: string, path: string) => Promise<void>;
11
+ rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
12
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
13
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ delete: (shardId: string, path: string) => Promise<void>;
15
+ };
8
16
 
9
17
  let {
10
18
  value = $bindable<OpenerValue>(null),
11
19
  listDocuments,
20
+ listFolders,
21
+ handle,
22
+ readOnlyShard,
23
+ selectable = 'file',
12
24
  disabled = false,
13
25
  invalid = false,
14
26
  size = 'md',
@@ -17,6 +29,10 @@
17
29
  }: {
18
30
  value?: OpenerValue;
19
31
  listDocuments: DocListFn;
32
+ listFolders?: FolderListFn;
33
+ handle?: HandleFn;
34
+ readOnlyShard?: (shardId: string) => boolean;
35
+ selectable?: 'file' | 'folder' | 'both';
20
36
  disabled?: boolean;
21
37
  invalid?: boolean;
22
38
  size?: 'sm' | 'md';
@@ -28,6 +44,10 @@
28
44
  mode="open"
29
45
  bind:value
30
46
  {listDocuments}
47
+ {listFolders}
48
+ {handle}
49
+ {readOnlyShard}
50
+ {selectable}
31
51
  {disabled}
32
52
  {invalid}
33
53
  {size}
@@ -4,9 +4,23 @@ import type { OpenerValue } from './DocumentFilePicker';
4
4
  type DocListFn = () => Promise<Array<DocumentMeta & {
5
5
  shardId: string;
6
6
  }>>;
7
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
8
+ type HandleFn = {
9
+ mkdir: (shardId: string, path: string) => Promise<void>;
10
+ rmdir: (shardId: string, path: string, opts: {
11
+ recursive: boolean;
12
+ }) => Promise<void>;
13
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
15
+ delete: (shardId: string, path: string) => Promise<void>;
16
+ };
7
17
  type $$ComponentProps = {
8
18
  value?: OpenerValue;
9
19
  listDocuments: DocListFn;
20
+ listFolders?: FolderListFn;
21
+ handle?: HandleFn;
22
+ readOnlyShard?: (shardId: string) => boolean;
23
+ selectable?: 'file' | 'folder' | 'both';
10
24
  disabled?: boolean;
11
25
  invalid?: boolean;
12
26
  size?: 'sm' | 'md';
@@ -5,10 +5,21 @@
5
5
  import type { SaverValue } from './DocumentFilePicker';
6
6
 
7
7
  type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
8
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
9
+ type HandleFn = {
10
+ mkdir: (shardId: string, path: string) => Promise<void>;
11
+ rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
12
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
13
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ delete: (shardId: string, path: string) => Promise<void>;
15
+ };
8
16
 
9
17
  let {
10
18
  value = $bindable<SaverValue>(null),
11
19
  listDocuments,
20
+ listFolders,
21
+ handle,
22
+ readOnlyShard,
12
23
  disabled = false,
13
24
  invalid = false,
14
25
  size = 'md',
@@ -17,6 +28,9 @@
17
28
  }: {
18
29
  value?: SaverValue;
19
30
  listDocuments: DocListFn;
31
+ listFolders?: FolderListFn;
32
+ handle?: HandleFn;
33
+ readOnlyShard?: (shardId: string) => boolean;
20
34
  disabled?: boolean;
21
35
  invalid?: boolean;
22
36
  size?: 'sm' | 'md';
@@ -28,6 +42,9 @@
28
42
  mode="save"
29
43
  bind:value
30
44
  {listDocuments}
45
+ {listFolders}
46
+ {handle}
47
+ {readOnlyShard}
31
48
  {disabled}
32
49
  {invalid}
33
50
  {size}
@@ -4,9 +4,22 @@ import type { SaverValue } from './DocumentFilePicker';
4
4
  type DocListFn = () => Promise<Array<DocumentMeta & {
5
5
  shardId: string;
6
6
  }>>;
7
+ type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
8
+ type HandleFn = {
9
+ mkdir: (shardId: string, path: string) => Promise<void>;
10
+ rmdir: (shardId: string, path: string, opts: {
11
+ recursive: boolean;
12
+ }) => Promise<void>;
13
+ renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
14
+ rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
15
+ delete: (shardId: string, path: string) => Promise<void>;
16
+ };
7
17
  type $$ComponentProps = {
8
18
  value?: SaverValue;
9
19
  listDocuments: DocListFn;
20
+ listFolders?: FolderListFn;
21
+ handle?: HandleFn;
22
+ readOnlyShard?: (shardId: string) => boolean;
10
23
  disabled?: boolean;
11
24
  invalid?: boolean;
12
25
  size?: 'sm' | 'md';
@@ -65,6 +65,7 @@
65
65
  <label class="sh3-picker__row">
66
66
  <input
67
67
  type="checkbox"
68
+ class="sh3-base-check"
68
69
  checked={value.includes(item.id)}
69
70
  {disabled}
70
71
  onchange={() => toggleId(item.id)}