@statewalker/file-panels.core 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{year}} Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ # @statewalker/file-panels.core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## What it is
4
+
5
+ The logic behind a file panel, without any rendering: a `PanelModel` that lists one folder of the
6
+ mounted storages into `@statewalker/explorer.core`'s `FilesListModel`, a safe copy and move across
7
+ storages with byte progress, file operations (create folder, rename, delete, transfer) that report
8
+ what they changed, a breadcrumb, desktop-drop import, and the interfaces a search provider
9
+ implements. React-free.
10
+
11
+ ## Why it exists
12
+
13
+ `FilesListModel` holds entries, sort, filter, cursor and selection, but does no I/O and knows
14
+ nothing about storages that can be locked or unreachable. A panel manager needs more: the reason a
15
+ folder cannot be read, the nearest folder that still exists when one disappears, refreshes when
16
+ another panel changes the same folder, and copies between a local folder and an S3 bucket that
17
+ never leave a half-written target behind. This package is that layer, so the views stay thin.
18
+
19
+ ## How to use
20
+
21
+ ```ts
22
+ import { newFileChanges } from "@statewalker/file-mounts.core";
23
+ import { FileOperations, PanelModel } from "@statewalker/file-panels.core";
24
+
25
+ const changes = newFileChanges(); // the one stream of changes
26
+ const ops = new FileOperations(files, changes); // files: the mounted storages (a FilesApi)
27
+ const panel = new PanelModel({
28
+ id: "left",
29
+ files,
30
+ changes,
31
+ mountNames: () => ({ docs: "Documents" }), // display names for the first path segment
32
+ });
33
+
34
+ await panel.navigate("/docs/clients");
35
+ panel.list.getVisibleEntries(); // what the view shows
36
+ panel.problem; // set when the folder cannot be read
37
+ panel.breadcrumb(); // [{ name: "Documents", path: "/docs", isMount: true }, ...]
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### A panel shows why a folder cannot be read
43
+
44
+ ```ts
45
+ await panel.navigate("/archive");
46
+ panel.problem; // { cause: "locked", message: "Locked: unlock to use the S3 storages." }
47
+ panel.list.entries; // [] : a locked storage is a problem, not an empty folder
48
+ ```
49
+
50
+ ### A folder that vanished moves the panel up, with a notice
51
+
52
+ ```ts
53
+ await panel.navigate("/docs/clients/gone/deeper");
54
+ panel.state.path; // "/docs/clients"
55
+ panel.notice; // "deeper no longer exists."
56
+ ```
57
+
58
+ ### Copy and move with progress and cancel
59
+
60
+ ```ts
61
+ import { planTransfer } from "@statewalker/file-panels.core"; // only for the clash dialog
62
+
63
+ const controller = new AbortController();
64
+ const { plan, result } = await ops.transfer(
65
+ { op: "move", sources: ["/docs/a.pdf"], targetDir: "/archive/2026", clash: "keep-both" },
66
+ { signal: controller.signal, onProgress: (p) => console.log(p.bytesDone, "/", p.bytesTotal) },
67
+ );
68
+ result.failed; // [{ path, problem: { message } }]: one entry per step that did not complete
69
+ ```
70
+
71
+ `transfer` throws `plan.invalid` (for example "A folder can't be copied or moved into itself.")
72
+ before touching anything, and throws when the target can't be listed for planning (a locked or
73
+ unreachable storage). Once steps run it does not throw: a storage that goes away mid-run fails the
74
+ remaining steps in `result.failed`, and the steps already done stay in `result.done`. To show the clash dialog first, call `planTransfer(files, request)`
75
+ yourself: `plan.clashes` names the taken names.
76
+
77
+ ### Folder operations
78
+
79
+ ```ts
80
+ await ops.mkdir("/docs", "reports"); // returns the new path; refuses a taken name
81
+ await ops.rename("/docs/a.pdf", "b.pdf"); // returns the new path
82
+ await ops.remove(["/docs/b.pdf"]);
83
+ ```
84
+
85
+ ### A desktop drop becomes a source
86
+
87
+ ```ts
88
+ import { importDropped } from "@statewalker/file-panels.core";
89
+
90
+ element.addEventListener("drop", async (event) => {
91
+ event.preventDefault();
92
+ const dropped = await importDropped(event.dataTransfer); // { files, paths }
93
+ await ops.transfer(
94
+ { op: "copy", sources: dropped.paths, targetDir: panel.state.path, clash: "keep-both" },
95
+ { source: dropped.files },
96
+ );
97
+ });
98
+ ```
99
+
100
+ `sources` are paths in `source`, the target is in the operations' own files. For the run,
101
+ `FileOperations` mounts `source` at `/.drop` beside its files, so the returned `plan` and `result`
102
+ name the dropped items as `/.drop/drop/...`. Passing the drop's `MemFilesApi` as the operations'
103
+ files instead would copy inside that throwaway storage and write nothing to the workspace.
104
+
105
+ ### Persisting the layout
106
+
107
+ `panel.state` is `{ id, path, sort: { field, descending } }`: what a dock layout saves and restores.
108
+
109
+ ## Internals
110
+
111
+ ### The transfer never leaves a partial or silently replaced target
112
+
113
+ Each step is written under a hidden sibling name (`.<name>.part-<random>`) and swapped into place
114
+ only when complete. This is the reason a cancel or a failure leaves nothing partial under the real
115
+ name, and why an overwritten folder is replaced rather than merged: the old one is set aside
116
+ (`.<name>.old-<random>`), the new one moved in, then the old one removed. Copying straight into the
117
+ target would merge folders and leave half a file behind on cancel; neither is acceptable when the
118
+ target is the only copy of someone's data.
119
+
120
+ What the person sees when something goes differently than planned:
121
+
122
+ | Situation | Message |
123
+ | --- | --- |
124
+ | The name was free when planned and taken since, without overwrite | "`name` appeared there meanwhile; nothing was replaced." |
125
+ | A move copied everything, but the source changed or grew while it ran | "It changed during the move: the copy is done, and `from` keeps what changed." |
126
+ | The source is gone when the step starts or ends | "It no longer exists." |
127
+ | The item is the target or contains it | "An item can't be copied or moved into itself." |
128
+ | Swapping in failed and the old target could not be put back | the cause, then "The old version is kept as `.name.old-xxxx`." |
129
+ | Rename or create onto a taken name | "`name` already exists here." |
130
+
131
+ A move deletes only what it copied unchanged: each source file is removed only if its size and
132
+ modification time still match what was copied, and folders only once empty. A failed step does not
133
+ stop the others; `result.failed` has one entry per failed step and `result.done` the rest. Cancel
134
+ returns `result.cancelled: true` and removes the temporary sibling of the file in flight.
135
+
136
+ The hidden names matter when they surface: a crash, or a failed rollback, can leave a `.name.part-*`
137
+ or `.name.old-*` folder next to the target. The latter holds the previous version and the failed
138
+ rollback message names its path.
139
+
140
+ ### Overwriting a folder on S3 costs a server-side copy and delete per object
141
+
142
+ S3 has no rename. The final move of the temporary sibling into place is a server-side copy followed
143
+ by a delete for every object under it, so replacing a large folder on a bucket takes time
144
+ proportional to its object count, even though no bytes are downloaded. Within one storage a copy
145
+ is delegated to the backend (`files.copy` / `files.move`) and overwrites go through the same
146
+ temporary-sibling swap.
147
+
148
+ ### The remaining window
149
+
150
+ Whether a target exists is checked just before the write or the swap, not atomically with it. Two
151
+ writers racing for the same name inside that gap can still replace each other. Closing it needs
152
+ conditional writes (`If-None-Match`) from the storage itself, which `FilesApi` does not expose.
153
+
154
+ ### The change stream and who listens
155
+
156
+ `FileOperations` emits `FileChange` events (`added`, `updated`, `deleted`) on the hub it is given,
157
+ reporting what is really on disk: a failed transfer step still reports a target that was written
158
+ and a source that was partly cleaned. When the storage can't be asked any more (it became
159
+ unavailable mid-run), the target and source of a failed step are reported as `updated`, so a panel
160
+ refreshes and shows either the files or the storage's problem. With `opts.source` (a drop) no
161
+ source-side events are emitted, since those paths mean nothing to listeners.
162
+
163
+ After an unlock or a reconnect, the mount table's `apply` returns the changed mount roots; pass them
164
+ on with `changes.emit(await table.apply(configs))`. Without that, a panel showing
165
+ "Locked: unlock to use the S3 storages." keeps showing it until it is navigated again. `PanelModel` subscribes to the optional `changes` and
166
+ refreshes when a change is inside the folder it was last asked to show, is that folder itself, or
167
+ is one of its ancestors; any other change is ignored. The asked-for folder counts even while its
168
+ listing is still loading, so a change in the previous folder never cancels the navigation; after a
169
+ fallback to an ancestor, the folder shown becomes the one that is tracked. Call `dispose()` to
170
+ unsubscribe; it also makes any listing still in flight be ignored, so a disposed panel never writes
171
+ to its list or notifies. A later indexer is meant to follow the same stream.
172
+
173
+ ### Newest navigation wins
174
+
175
+ `navigate` lists the whole folder before touching the `FilesListModel`, and each call carries a
176
+ sequence number. A slow listing that finishes after a newer `navigate` returns without writing, so
177
+ the newer folder's entries are never replaced. Listing first also means the list is not blanked
178
+ for a folder that turns out to be unreadable until the result is known.
179
+
180
+ ### Why `importDropped` reads entries before the first `await`
181
+
182
+ Browsers empty a `DataTransfer` once the drop handler returns synchronously. `importDropped` takes
183
+ `webkitGetAsEntry()` for every item first, then reads the files asynchronously into a `MemFilesApi`
184
+ under `/drop/...`. If it awaited first, the drop would look empty. Where the browser has no
185
+ entries API only the plain files are taken, so dropped folders are lost there.
186
+
187
+ ### Search is an interface only
188
+
189
+ `SearchProvider` (`id`, `search(query, { scope, signal })` yielding `SearchHit`) and `SearchHit`
190
+ (`path`, `title`, `snippet?`, `score`) are types; nothing in this package implements them. They are
191
+ the seam where a full-text index over the mounts plugs in later.
192
+
193
+ ### Constraints and dependencies
194
+
195
+ - `runTransfer` decides whether a step crosses storages by the first path segment (a cross-storage
196
+ step is streamed through a temporary sibling, an in-storage one delegated to the backend); its
197
+ `mountOf` option overrides that when the mount table differs.
198
+ - The target folder must be inside a storage: `/` is refused ("Choose a folder inside a storage.").
199
+ - Depends on `@statewalker/explorer.core` (the list model), `@statewalker/file-mounts.core`
200
+ (`FileChanges`, `Problem`, `problemOf`), `@statewalker/shared-baseclass` (`BaseClass`),
201
+ `@statewalker/webrun-files` (`FilesApi`, path helpers), `@statewalker/webrun-files-mem`
202
+ (the drop's in-memory storage) and `@statewalker/webrun-files-composite` (mounting a drop beside
203
+ the storages for a transfer).
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,32 @@
1
+ import { TransferPlan, TransferRequest } from "./transfer-plan.js";
2
+ import { TransferProgress, TransferResult } from "./transfer-run.js";
3
+ import { FilesApi } from "@statewalker/webrun-files";
4
+ import { FileChangesHub } from "@statewalker/file-mounts.core";
5
+ //#region src/file-operations.d.ts
6
+ export declare class FileOperations {
7
+ readonly files: FilesApi;
8
+ readonly changes: FileChangesHub;
9
+ constructor(files: FilesApi, changes: FileChangesHub);
10
+ /**
11
+ * Plans and runs a copy or move. `source` is a foreign source (a desktop drop): its
12
+ * paths are taken from it, mounted at `/.drop` beside these files for the run.
13
+ */
14
+ transfer(request: TransferRequest, opts?: {
15
+ source?: FilesApi;
16
+ signal?: AbortSignal;
17
+ onProgress?(p: TransferProgress): void;
18
+ }): Promise<{
19
+ plan: TransferPlan;
20
+ result: TransferResult;
21
+ }>;
22
+ /** Whether `path` exists; `undefined` when its storage went away mid-run and can't say. */
23
+ private probe;
24
+ mkdir(dir: string, name: string): Promise<string>;
25
+ /** Never replaces: a name taken since the dialog opened is refused. */
26
+ rename(path: string, name: string): Promise<string>;
27
+ /** Reports the paths actually removed, even when a later one fails. */
28
+ remove(paths: string[]): Promise<void>;
29
+ private freePath;
30
+ }
31
+ //#endregion
32
+ //# sourceMappingURL=file-operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-operations.d.ts","names":[],"sources":["../src/file-operations.ts"],"mappings":";;;;;qBAiBa;WAEA,OAAO;WACP,SAAS;EAFpB,YACW,OAAO,UACP,SAAS;;;;;EAOd,SACJ,SAAS,iBACT;IAAQ,SAAS;IAAU,SAAS;IAAa,YAAY,GAAG;MAC/D;IAAU,MAAM;IAAc,QAAQ;;;UA0B3B;EAQR,MAAM,aAAa,eAAe;;EAQlC,OAAO,cAAc,eAAe;;EAWpC,OAAO,kBAAkB;UAWjB"}
@@ -0,0 +1,106 @@
1
+ import { planTransfer, validateName } from "./transfer-plan.js";
2
+ import { runTransfer } from "./transfer-run.js";
3
+ import { dirname, joinPath } from "@statewalker/webrun-files";
4
+ import { CompositeFilesApi } from "@statewalker/webrun-files-composite";
5
+ //#region src/file-operations.ts
6
+ /** Where a foreign source is mounted while a transfer runs. */
7
+ const DROP = "/.drop";
8
+ var FileOperations = class {
9
+ files;
10
+ changes;
11
+ constructor(files, changes) {
12
+ this.files = files;
13
+ this.changes = changes;
14
+ }
15
+ /**
16
+ * Plans and runs a copy or move. `source` is a foreign source (a desktop drop): its
17
+ * paths are taken from it, mounted at `/.drop` beside these files for the run.
18
+ */
19
+ async transfer(request, opts = {}) {
20
+ const { source } = opts;
21
+ const files = source ? new CompositeFilesApi(this.files).mount(DROP, source) : this.files;
22
+ if (source) request = {
23
+ ...request,
24
+ sources: request.sources.map((s) => joinPath(DROP, s))
25
+ };
26
+ const plan = await planTransfer(files, request);
27
+ if (plan.invalid) throw new Error(plan.invalid);
28
+ const result = await runTransfer(files, plan, opts);
29
+ const failed = new Set(result.failed.map((f) => f.path));
30
+ const changes = [];
31
+ for (const step of plan.steps) {
32
+ const done = result.done.includes(step.from);
33
+ const target = done || !failed.has(step.from) ? done : await this.probe(step.to);
34
+ if (target === false) continue;
35
+ const written = step.overwrite ? "updated" : "added";
36
+ changes.push({
37
+ type: target === void 0 ? "updated" : written,
38
+ path: step.to
39
+ });
40
+ if (step.op !== "move" || source) continue;
41
+ const stays = !done && await this.probe(step.from) !== false;
42
+ changes.push({
43
+ type: stays ? "updated" : "deleted",
44
+ path: step.from
45
+ });
46
+ }
47
+ this.changes.emit(changes);
48
+ return {
49
+ plan,
50
+ result
51
+ };
52
+ }
53
+ /** Whether `path` exists; `undefined` when its storage went away mid-run and can't say. */
54
+ async probe(path) {
55
+ try {
56
+ return await this.files.exists(path);
57
+ } catch {
58
+ return;
59
+ }
60
+ }
61
+ async mkdir(dir, name) {
62
+ const path = await this.freePath(dir, name);
63
+ await this.files.mkdir(path);
64
+ this.changes.emit([{
65
+ type: "added",
66
+ path
67
+ }]);
68
+ return path;
69
+ }
70
+ /** Never replaces: a name taken since the dialog opened is refused. */
71
+ async rename(path, name) {
72
+ const to = await this.freePath(dirname(path), name);
73
+ if (!await this.files.move(path, to)) throw new Error("It no longer exists.");
74
+ this.changes.emit([{
75
+ type: "deleted",
76
+ path
77
+ }, {
78
+ type: "added",
79
+ path: to
80
+ }]);
81
+ return to;
82
+ }
83
+ /** Reports the paths actually removed, even when a later one fails. */
84
+ async remove(paths) {
85
+ const removed = [];
86
+ try {
87
+ for (const path of paths) if (await this.files.remove(path)) removed.push({
88
+ type: "deleted",
89
+ path
90
+ });
91
+ } finally {
92
+ this.changes.emit(removed);
93
+ }
94
+ }
95
+ async freePath(dir, name) {
96
+ const invalid = validateName(name);
97
+ if (invalid) throw new Error(invalid);
98
+ const path = joinPath(dir, name);
99
+ if (await this.files.exists(path)) throw new Error(`${name} already exists here.`);
100
+ return path;
101
+ }
102
+ };
103
+ //#endregion
104
+ export { FileOperations };
105
+
106
+ //# sourceMappingURL=file-operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-operations.js","names":[],"sources":["../src/file-operations.ts"],"sourcesContent":["// The operations a panel offers, each reporting what it changed on the one\n// change stream: panels refresh from it, an indexer will follow it.\nimport type { FileChange, FileChangesHub } from \"@statewalker/file-mounts.core\";\nimport type { FilesApi } from \"@statewalker/webrun-files\";\nimport { dirname, joinPath } from \"@statewalker/webrun-files\";\nimport { CompositeFilesApi } from \"@statewalker/webrun-files-composite\";\nimport {\n planTransfer,\n type TransferPlan,\n type TransferRequest,\n validateName,\n} from \"./transfer-plan.js\";\nimport { runTransfer, type TransferProgress, type TransferResult } from \"./transfer-run.js\";\n\n/** Where a foreign source is mounted while a transfer runs. */\nconst DROP = \"/.drop\";\n\nexport class FileOperations {\n constructor(\n readonly files: FilesApi,\n readonly changes: FileChangesHub,\n ) {}\n\n /**\n * Plans and runs a copy or move. `source` is a foreign source (a desktop drop): its\n * paths are taken from it, mounted at `/.drop` beside these files for the run.\n */\n async transfer(\n request: TransferRequest,\n opts: { source?: FilesApi; signal?: AbortSignal; onProgress?(p: TransferProgress): void } = {},\n ): Promise<{ plan: TransferPlan; result: TransferResult }> {\n const { source } = opts;\n const files = source ? new CompositeFilesApi(this.files).mount(DROP, source) : this.files;\n if (source) request = { ...request, sources: request.sources.map((s) => joinPath(DROP, s)) };\n const plan = await planTransfer(files, request);\n if (plan.invalid) throw new Error(plan.invalid);\n const result = await runTransfer(files, plan, opts);\n const failed = new Set(result.failed.map((f) => f.path));\n const changes: FileChange[] = [];\n for (const step of plan.steps) {\n const done = result.done.includes(step.from);\n // A failed step may have written its target or cleaned part of its source: look.\n const target = done || !failed.has(step.from) ? done : await this.probe(step.to);\n if (target === false) continue;\n const written = step.overwrite ? \"updated\" : \"added\";\n changes.push({ type: target === undefined ? \"updated\" : written, path: step.to });\n // Source-side paths of a foreign source mean nothing to listeners.\n if (step.op !== \"move\" || source) continue;\n const stays = !done && (await this.probe(step.from)) !== false;\n changes.push({ type: stays ? \"updated\" : \"deleted\", path: step.from });\n }\n this.changes.emit(changes);\n return { plan, result };\n }\n\n /** Whether `path` exists; `undefined` when its storage went away mid-run and can't say. */\n private async probe(path: string): Promise<boolean | undefined> {\n try {\n return await this.files.exists(path);\n } catch {\n return undefined;\n }\n }\n\n async mkdir(dir: string, name: string): Promise<string> {\n const path = await this.freePath(dir, name);\n await this.files.mkdir(path);\n this.changes.emit([{ type: \"added\", path }]);\n return path;\n }\n\n /** Never replaces: a name taken since the dialog opened is refused. */\n async rename(path: string, name: string): Promise<string> {\n const to = await this.freePath(dirname(path), name);\n if (!(await this.files.move(path, to))) throw new Error(\"It no longer exists.\");\n this.changes.emit([\n { type: \"deleted\", path },\n { type: \"added\", path: to },\n ]);\n return to;\n }\n\n /** Reports the paths actually removed, even when a later one fails. */\n async remove(paths: string[]): Promise<void> {\n const removed: FileChange[] = [];\n try {\n for (const path of paths) {\n if (await this.files.remove(path)) removed.push({ type: \"deleted\", path });\n }\n } finally {\n this.changes.emit(removed);\n }\n }\n\n private async freePath(dir: string, name: string): Promise<string> {\n const invalid = validateName(name);\n if (invalid) throw new Error(invalid);\n const path = joinPath(dir, name);\n if (await this.files.exists(path)) throw new Error(`${name} already exists here.`);\n return path;\n }\n}\n"],"mappings":";;;;;;AAeA,MAAM,OAAO;AAEb,IAAa,iBAAb,MAA4B;CAEf;CACA;CAFX,YACE,OACA,SACA;EAFS,KAAA,QAAA;EACA,KAAA,UAAA;CACR;;;;;CAMH,MAAM,SACJ,SACA,OAA4F,CAAC,GACpC;EACzD,MAAM,EAAE,WAAW;EACnB,MAAM,QAAQ,SAAS,IAAI,kBAAkB,KAAK,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM,IAAI,KAAK;EACpF,IAAI,QAAQ,UAAU;GAAE,GAAG;GAAS,SAAS,QAAQ,QAAQ,KAAK,MAAM,SAAS,MAAM,CAAC,CAAC;EAAE;EAC3F,MAAM,OAAO,MAAM,aAAa,OAAO,OAAO;EAC9C,IAAI,KAAK,SAAS,MAAM,IAAI,MAAM,KAAK,OAAO;EAC9C,MAAM,SAAS,MAAM,YAAY,OAAO,MAAM,IAAI;EAClD,MAAM,SAAS,IAAI,IAAI,OAAO,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;EACvD,MAAM,UAAwB,CAAC;EAC/B,KAAK,MAAM,QAAQ,KAAK,OAAO;GAC7B,MAAM,OAAO,OAAO,KAAK,SAAS,KAAK,IAAI;GAE3C,MAAM,SAAS,QAAQ,CAAC,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,MAAM,KAAK,EAAE;GAC/E,IAAI,WAAW,OAAO;GACtB,MAAM,UAAU,KAAK,YAAY,YAAY;GAC7C,QAAQ,KAAK;IAAE,MAAM,WAAW,KAAA,IAAY,YAAY;IAAS,MAAM,KAAK;GAAG,CAAC;GAEhF,IAAI,KAAK,OAAO,UAAU,QAAQ;GAClC,MAAM,QAAQ,CAAC,QAAS,MAAM,KAAK,MAAM,KAAK,IAAI,MAAO;GACzD,QAAQ,KAAK;IAAE,MAAM,QAAQ,YAAY;IAAW,MAAM,KAAK;GAAK,CAAC;EACvE;EACA,KAAK,QAAQ,KAAK,OAAO;EACzB,OAAO;GAAE;GAAM;EAAO;CACxB;;CAGA,MAAc,MAAM,MAA4C;EAC9D,IAAI;GACF,OAAO,MAAM,KAAK,MAAM,OAAO,IAAI;EACrC,QAAQ;GACN;EACF;CACF;CAEA,MAAM,MAAM,KAAa,MAA+B;EACtD,MAAM,OAAO,MAAM,KAAK,SAAS,KAAK,IAAI;EAC1C,MAAM,KAAK,MAAM,MAAM,IAAI;EAC3B,KAAK,QAAQ,KAAK,CAAC;GAAE,MAAM;GAAS;EAAK,CAAC,CAAC;EAC3C,OAAO;CACT;;CAGA,MAAM,OAAO,MAAc,MAA+B;EACxD,MAAM,KAAK,MAAM,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI;EAClD,IAAI,CAAE,MAAM,KAAK,MAAM,KAAK,MAAM,EAAE,GAAI,MAAM,IAAI,MAAM,sBAAsB;EAC9E,KAAK,QAAQ,KAAK,CAChB;GAAE,MAAM;GAAW;EAAK,GACxB;GAAE,MAAM;GAAS,MAAM;EAAG,CAC5B,CAAC;EACD,OAAO;CACT;;CAGA,MAAM,OAAO,OAAgC;EAC3C,MAAM,UAAwB,CAAC;EAC/B,IAAI;GACF,KAAK,MAAM,QAAQ,OACjB,IAAI,MAAM,KAAK,MAAM,OAAO,IAAI,GAAG,QAAQ,KAAK;IAAE,MAAM;IAAW;GAAK,CAAC;EAE7E,UAAU;GACR,KAAK,QAAQ,KAAK,OAAO;EAC3B;CACF;CAEA,MAAc,SAAS,KAAa,MAA+B;EACjE,MAAM,UAAU,aAAa,IAAI;EACjC,IAAI,SAAS,MAAM,IAAI,MAAM,OAAO;EACpC,MAAM,OAAO,SAAS,KAAK,IAAI;EAC/B,IAAI,MAAM,KAAK,MAAM,OAAO,IAAI,GAAG,MAAM,IAAI,MAAM,GAAG,KAAK,sBAAsB;EACjF,OAAO;CACT;AACF"}
@@ -0,0 +1,13 @@
1
+ import { FilesApi } from "@statewalker/webrun-files";
2
+ //#region src/import-dropped.d.ts
3
+ /**
4
+ * Reads a drop into a `MemFilesApi` under `/drop/…`; `paths` are the dropped
5
+ * items themselves. Folders are read through `webkitGetAsEntry` when the
6
+ * browser offers it, otherwise only the plain `files` are taken.
7
+ */
8
+ export declare function importDropped(data: DataTransfer): Promise<{
9
+ files: FilesApi;
10
+ paths: string[];
11
+ }>;
12
+ //#endregion
13
+ //# sourceMappingURL=import-dropped.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-dropped.d.ts","names":[],"sources":["../src/import-dropped.ts"],"mappings":";;;;;;;wBAyBsB,cACpB,MAAM,eACL;EAAU,OAAO;EAAU"}
@@ -0,0 +1,46 @@
1
+ import { joinPath } from "@statewalker/webrun-files";
2
+ import { MemFilesApi } from "@statewalker/webrun-files-mem";
3
+ //#region src/import-dropped.ts
4
+ const ROOT = "/drop";
5
+ /**
6
+ * Reads a drop into a `MemFilesApi` under `/drop/…`; `paths` are the dropped
7
+ * items themselves. Folders are read through `webkitGetAsEntry` when the
8
+ * browser offers it, otherwise only the plain `files` are taken.
9
+ */
10
+ async function importDropped(data) {
11
+ const entries = Array.from(data.items ?? []).map((item) => item.webkitGetAsEntry?.()).filter((e) => Boolean(e));
12
+ const plain = Array.from(data.files ?? []);
13
+ const files = new MemFilesApi();
14
+ const write = async (path, file) => files.write(path, [new Uint8Array(await file.arrayBuffer())]);
15
+ if (!entries.length) {
16
+ for (const file of plain) await write(joinPath(ROOT, file.name), file);
17
+ return {
18
+ files,
19
+ paths: plain.map((f) => joinPath(ROOT, f.name))
20
+ };
21
+ }
22
+ const walk = async (entry) => {
23
+ const path = joinPath(ROOT, entry.fullPath);
24
+ if (entry.isFile && entry.file) {
25
+ const file = await new Promise((ok, fail) => entry.file?.(ok, fail));
26
+ await write(path, file);
27
+ } else if (entry.isDirectory && entry.createReader) {
28
+ await files.mkdir(path);
29
+ const reader = entry.createReader();
30
+ for (;;) {
31
+ const batch = await new Promise((ok, fail) => reader.readEntries(ok, fail));
32
+ if (!batch.length) break;
33
+ for (const child of batch) await walk(child);
34
+ }
35
+ }
36
+ };
37
+ for (const entry of entries) await walk(entry);
38
+ return {
39
+ files,
40
+ paths: entries.map((e) => joinPath(ROOT, e.fullPath))
41
+ };
42
+ }
43
+ //#endregion
44
+ export { importDropped };
45
+
46
+ //# sourceMappingURL=import-dropped.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-dropped.js","names":[],"sources":["../src/import-dropped.ts"],"sourcesContent":["// Files and folders dropped from the desktop, read into memory so the same\n// planner and runner copy them like any other source.\nimport type { FilesApi } from \"@statewalker/webrun-files\";\nimport { joinPath } from \"@statewalker/webrun-files\";\nimport { MemFilesApi } from \"@statewalker/webrun-files-mem\";\n\n// The parts of FileSystemEntry this reads; a test passes plain objects.\ninterface Entry {\n isFile: boolean;\n isDirectory: boolean;\n name: string;\n fullPath: string;\n file?(ok: (file: File) => void, fail?: (error: unknown) => void): void;\n createReader?(): {\n readEntries(ok: (entries: Entry[]) => void, fail?: (e: unknown) => void): void;\n };\n}\n\nconst ROOT = \"/drop\";\n\n/**\n * Reads a drop into a `MemFilesApi` under `/drop/…`; `paths` are the dropped\n * items themselves. Folders are read through `webkitGetAsEntry` when the\n * browser offers it, otherwise only the plain `files` are taken.\n */\nexport async function importDropped(\n data: DataTransfer,\n): Promise<{ files: FilesApi; paths: string[] }> {\n // Entries must be taken before the first await: the browser empties the\n // DataTransfer once the drop event handler returns.\n const entries = Array.from(data.items ?? [])\n .map((item) => item.webkitGetAsEntry?.() as Entry | null | undefined)\n .filter((e): e is Entry => Boolean(e));\n const plain = Array.from(data.files ?? []);\n const files = new MemFilesApi();\n const write = async (path: string, file: File) =>\n files.write(path, [new Uint8Array(await file.arrayBuffer())]);\n\n if (!entries.length) {\n for (const file of plain) await write(joinPath(ROOT, file.name), file);\n return { files, paths: plain.map((f) => joinPath(ROOT, f.name)) };\n }\n\n const walk = async (entry: Entry): Promise<void> => {\n const path = joinPath(ROOT, entry.fullPath);\n if (entry.isFile && entry.file) {\n const file = await new Promise<File>((ok, fail) => entry.file?.(ok, fail));\n await write(path, file);\n } else if (entry.isDirectory && entry.createReader) {\n await files.mkdir(path);\n const reader = entry.createReader();\n // readEntries returns a batch at a time, and an empty batch at the end.\n for (;;) {\n const batch = await new Promise<Entry[]>((ok, fail) => reader.readEntries(ok, fail));\n if (!batch.length) break;\n for (const child of batch) await walk(child);\n }\n }\n };\n for (const entry of entries) await walk(entry);\n return { files, paths: entries.map((e) => joinPath(ROOT, e.fullPath)) };\n}\n"],"mappings":";;;AAkBA,MAAM,OAAO;;;;;;AAOb,eAAsB,cACpB,MAC+C;CAG/C,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CACzC,KAAK,SAAS,KAAK,mBAAmB,CAA6B,CAAC,CACpE,QAAQ,MAAkB,QAAQ,CAAC,CAAC;CACvC,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC;CACzC,MAAM,QAAQ,IAAI,YAAY;CAC9B,MAAM,QAAQ,OAAO,MAAc,SACjC,MAAM,MAAM,MAAM,CAAC,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC;CAE9D,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK,MAAM,QAAQ,OAAO,MAAM,MAAM,SAAS,MAAM,KAAK,IAAI,GAAG,IAAI;EACrE,OAAO;GAAE;GAAO,OAAO,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,IAAI,CAAC;EAAE;CAClE;CAEA,MAAM,OAAO,OAAO,UAAgC;EAClD,MAAM,OAAO,SAAS,MAAM,MAAM,QAAQ;EAC1C,IAAI,MAAM,UAAU,MAAM,MAAM;GAC9B,MAAM,OAAO,MAAM,IAAI,SAAe,IAAI,SAAS,MAAM,OAAO,IAAI,IAAI,CAAC;GACzE,MAAM,MAAM,MAAM,IAAI;EACxB,OAAO,IAAI,MAAM,eAAe,MAAM,cAAc;GAClD,MAAM,MAAM,MAAM,IAAI;GACtB,MAAM,SAAS,MAAM,aAAa;GAElC,SAAS;IACP,MAAM,QAAQ,MAAM,IAAI,SAAkB,IAAI,SAAS,OAAO,YAAY,IAAI,IAAI,CAAC;IACnF,IAAI,CAAC,MAAM,QAAQ;IACnB,KAAK,MAAM,SAAS,OAAO,MAAM,KAAK,KAAK;GAC7C;EACF;CACF;CACA,KAAK,MAAM,SAAS,SAAS,MAAM,KAAK,KAAK;CAC7C,OAAO;EAAE;EAAO,OAAO,QAAQ,KAAK,MAAM,SAAS,MAAM,EAAE,QAAQ,CAAC;CAAE;AACxE"}
@@ -0,0 +1,8 @@
1
+ import { ClashPolicy, TransferOp, TransferPlan, TransferRequest, TransferStep, findClashes, freeName, planTransfer, validateName } from "./transfer-plan.js";
2
+ import { TransferProgress, TransferResult, runTransfer } from "./transfer-run.js";
3
+ import { FileOperations } from "./file-operations.js";
4
+ import { importDropped } from "./import-dropped.js";
5
+ import { Crumb, breadcrumb, siblings } from "./paths.js";
6
+ import { PanelModel, PanelState } from "./panel-model.js";
7
+ import { SearchHit, SearchProvider } from "./search.js";
8
+ export { ClashPolicy, Crumb, FileOperations, PanelModel, PanelState, type SearchHit, type SearchProvider, TransferOp, TransferPlan, TransferProgress, TransferRequest, TransferResult, TransferStep, breadcrumb, findClashes, freeName, importDropped, planTransfer, runTransfer, siblings, validateName };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { findClashes, freeName, planTransfer, validateName } from "./transfer-plan.js";
2
+ import { runTransfer } from "./transfer-run.js";
3
+ import { FileOperations } from "./file-operations.js";
4
+ import { importDropped } from "./import-dropped.js";
5
+ import { breadcrumb, siblings } from "./paths.js";
6
+ import { PanelModel } from "./panel-model.js";
7
+ export { FileOperations, PanelModel, breadcrumb, findClashes, freeName, importDropped, planTransfer, runTransfer, siblings, validateName };
@@ -0,0 +1,46 @@
1
+ import { Crumb } from "./paths.js";
2
+ import { FilesApi } from "@statewalker/webrun-files";
3
+ import { FileChanges, Problem } from "@statewalker/file-mounts.core";
4
+ import { FilesListModel } from "@statewalker/explorer.core";
5
+ import { BaseClass } from "@statewalker/shared-baseclass";
6
+ //#region src/panel-model.d.ts
7
+ export interface PanelState {
8
+ id: string;
9
+ path: string;
10
+ sort: {
11
+ field: FilesListModel["sortField"];
12
+ descending: boolean;
13
+ };
14
+ }
15
+ export declare class PanelModel extends BaseClass {
16
+ readonly id: string;
17
+ readonly list: FilesListModel;
18
+ problem?: Problem;
19
+ notice?: string;
20
+ private loads;
21
+ /** The folder most recently asked for; `list.path` only changes when a load completes. */
22
+ private target;
23
+ private readonly files;
24
+ private readonly mountNames;
25
+ private readonly unsubscribe?;
26
+ constructor(opts: {
27
+ id: string;
28
+ files: FilesApi;
29
+ changes?: FileChanges;
30
+ mountNames?: () => Record<string, string>;
31
+ });
32
+ get state(): PanelState;
33
+ /** Lists `path`; a newer navigation wins over a slower older one. */
34
+ navigate(path: string): Promise<void>;
35
+ up(): Promise<void>;
36
+ refresh(): Promise<void>;
37
+ setSort(field: FilesListModel["sortField"]): void;
38
+ breadcrumb(): Crumb[];
39
+ siblings(crumb: Crumb): Promise<{
40
+ name: string;
41
+ path: string;
42
+ }[]>;
43
+ dispose(): void;
44
+ }
45
+ //#endregion
46
+ //# sourceMappingURL=panel-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-model.d.ts","names":[],"sources":["../src/panel-model.ts"],"mappings":";;;;;;iBAWiB;EACf;EACA;EACA;IAAQ,OAAO;IAA6B;;;qBAOjC,mBAAmB;WACrB;WACA,MAAI;EACb,UAAU;EACV;UACQ;;UAEA;mBACS;mBACA;mBACA;EAEjB,YAAY;IACV;IACA,OAAO;IACP,UAAU;IACV,mBAAmB;;MAWjB,SAAS;;EASP,SAAS,eAAe;EA2B9B,MAAM;EAIN,WAAW;EAIX,QAAQ,OAAO;EAKf,cAAc;EAId,SAAS,OAAO,QAAK;;;;EAIrB"}
@@ -0,0 +1,88 @@
1
+ import { breadcrumb, siblings } from "./paths.js";
2
+ import { basename, dirname, normalizePath } from "@statewalker/webrun-files";
3
+ import { problemOf } from "@statewalker/file-mounts.core";
4
+ import { FilesListModel } from "@statewalker/explorer.core";
5
+ import { BaseClass } from "@statewalker/shared-baseclass";
6
+ //#region src/panel-model.ts
7
+ /** A change at `changed` is visible in `folder`: inside it, the folder itself, or an ancestor. */
8
+ const touches = (changed, folder) => dirname(changed) === folder || changed === folder || folder.startsWith(`${changed}/`);
9
+ var PanelModel = class extends BaseClass {
10
+ id;
11
+ list = new FilesListModel();
12
+ problem;
13
+ notice;
14
+ loads = 0;
15
+ /** The folder most recently asked for; `list.path` only changes when a load completes. */
16
+ target = "/";
17
+ files;
18
+ mountNames;
19
+ unsubscribe;
20
+ constructor(opts) {
21
+ super();
22
+ this.id = opts.id;
23
+ this.files = opts.files;
24
+ this.mountNames = opts.mountNames ?? (() => ({}));
25
+ this.unsubscribe = opts.changes?.subscribe((changes) => {
26
+ if (changes.some((c) => touches(c.path, this.target))) this.refresh();
27
+ });
28
+ }
29
+ get state() {
30
+ return {
31
+ id: this.id,
32
+ path: this.list.path,
33
+ sort: {
34
+ field: this.list.sortField,
35
+ descending: !this.list.sortAscending
36
+ }
37
+ };
38
+ }
39
+ /** Lists `path`; a newer navigation wins over a slower older one. */
40
+ async navigate(path) {
41
+ const load = ++this.loads;
42
+ const wanted = normalizePath(path);
43
+ this.target = wanted;
44
+ let notice;
45
+ let problem;
46
+ let at = wanted;
47
+ let entries;
48
+ try {
49
+ while (at !== "/" && (await this.files.stats(at))?.kind !== "directory") at = dirname(at);
50
+ if (at !== wanted) notice = `${basename(wanted)} no longer exists.`;
51
+ const listed = [];
52
+ for await (const entry of this.files.list(at)) listed.push(entry);
53
+ entries = listed;
54
+ } catch (error) {
55
+ problem = problemOf(error);
56
+ }
57
+ if (load !== this.loads) return;
58
+ this.target = at;
59
+ this.list.startLoading({ path: at })(problem ? { error: problem.message } : { entries });
60
+ this.problem = problem;
61
+ this.notice = notice;
62
+ this.notify();
63
+ }
64
+ up() {
65
+ return this.navigate(dirname(this.target));
66
+ }
67
+ refresh() {
68
+ return this.navigate(this.target);
69
+ }
70
+ setSort(field) {
71
+ this.list.setSort(field);
72
+ this.notify();
73
+ }
74
+ breadcrumb() {
75
+ return breadcrumb(this.list.path, this.mountNames());
76
+ }
77
+ siblings(crumb) {
78
+ return siblings(this.files, crumb);
79
+ }
80
+ dispose() {
81
+ this.loads++;
82
+ this.unsubscribe?.();
83
+ }
84
+ };
85
+ //#endregion
86
+ export { PanelModel };
87
+
88
+ //# sourceMappingURL=panel-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-model.js","names":[],"sources":["../src/panel-model.ts"],"sourcesContent":["// One file panel: a folder of the mounted storages listed into explorer.core's\n// FilesListModel (entries, sort, filter, cursor, selection), plus what a\n// multi-panel manager needs: the breadcrumb, the reason a folder can't be read,\n// and refreshes from the change stream.\nimport { FilesListModel } from \"@statewalker/explorer.core\";\nimport { type FileChanges, type Problem, problemOf } from \"@statewalker/file-mounts.core\";\nimport { BaseClass } from \"@statewalker/shared-baseclass\";\nimport type { FileInfo, FilesApi } from \"@statewalker/webrun-files\";\nimport { basename, dirname, normalizePath } from \"@statewalker/webrun-files\";\nimport { breadcrumb, type Crumb, siblings } from \"./paths.js\";\n\nexport interface PanelState {\n id: string;\n path: string;\n sort: { field: FilesListModel[\"sortField\"]; descending: boolean };\n}\n\n/** A change at `changed` is visible in `folder`: inside it, the folder itself, or an ancestor. */\nconst touches = (changed: string, folder: string) =>\n dirname(changed) === folder || changed === folder || folder.startsWith(`${changed}/`);\n\nexport class PanelModel extends BaseClass {\n readonly id: string;\n readonly list = new FilesListModel();\n problem?: Problem;\n notice?: string;\n private loads = 0;\n /** The folder most recently asked for; `list.path` only changes when a load completes. */\n private target = \"/\";\n private readonly files: FilesApi;\n private readonly mountNames: () => Record<string, string>;\n private readonly unsubscribe?: () => void;\n\n constructor(opts: {\n id: string;\n files: FilesApi;\n changes?: FileChanges;\n mountNames?: () => Record<string, string>;\n }) {\n super();\n this.id = opts.id;\n this.files = opts.files;\n this.mountNames = opts.mountNames ?? (() => ({}));\n this.unsubscribe = opts.changes?.subscribe((changes) => {\n if (changes.some((c) => touches(c.path, this.target))) void this.refresh();\n });\n }\n\n get state(): PanelState {\n return {\n id: this.id,\n path: this.list.path,\n sort: { field: this.list.sortField, descending: !this.list.sortAscending },\n };\n }\n\n /** Lists `path`; a newer navigation wins over a slower older one. */\n async navigate(path: string): Promise<void> {\n const load = ++this.loads;\n const wanted = normalizePath(path);\n this.target = wanted;\n let notice: string | undefined;\n let problem: Problem | undefined;\n let at = wanted;\n let entries: FileInfo[] | undefined;\n try {\n // A folder that is gone sends the panel to its nearest existing ancestor.\n while (at !== \"/\" && (await this.files.stats(at))?.kind !== \"directory\") at = dirname(at);\n if (at !== wanted) notice = `${basename(wanted)} no longer exists.`;\n const listed: FileInfo[] = [];\n for await (const entry of this.files.list(at)) listed.push(entry);\n entries = listed;\n } catch (error) {\n problem = problemOf(error);\n }\n if (load !== this.loads) return;\n this.target = at; // after a fallback, the folder shown is the one to refresh and go up from\n const done = this.list.startLoading({ path: at });\n done(problem ? { error: problem.message } : { entries });\n this.problem = problem;\n this.notice = notice;\n this.notify();\n }\n\n up(): Promise<void> {\n return this.navigate(dirname(this.target));\n }\n\n refresh(): Promise<void> {\n return this.navigate(this.target);\n }\n\n setSort(field: FilesListModel[\"sortField\"]) {\n this.list.setSort(field);\n this.notify();\n }\n\n breadcrumb(): Crumb[] {\n return breadcrumb(this.list.path, this.mountNames());\n }\n\n siblings(crumb: Crumb) {\n return siblings(this.files, crumb);\n }\n\n dispose() {\n this.loads++; // an in-flight navigation is ignored from now on\n this.unsubscribe?.();\n }\n}\n"],"mappings":";;;;;;;AAkBA,MAAM,WAAW,SAAiB,WAChC,QAAQ,OAAO,MAAM,UAAU,YAAY,UAAU,OAAO,WAAW,GAAG,QAAQ,EAAE;AAEtF,IAAa,aAAb,cAAgC,UAAU;CACxC;CACA,OAAgB,IAAI,eAAe;CACnC;CACA;CACA,QAAgB;;CAEhB,SAAiB;CACjB;CACA;CACA;CAEA,YAAY,MAKT;EACD,MAAM;EACN,KAAK,KAAK,KAAK;EACf,KAAK,QAAQ,KAAK;EAClB,KAAK,aAAa,KAAK,sBAAsB,CAAC;EAC9C,KAAK,cAAc,KAAK,SAAS,WAAW,YAAY;GACtD,IAAI,QAAQ,MAAM,MAAM,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,KAAU,QAAQ;EAC3E,CAAC;CACH;CAEA,IAAI,QAAoB;EACtB,OAAO;GACL,IAAI,KAAK;GACT,MAAM,KAAK,KAAK;GAChB,MAAM;IAAE,OAAO,KAAK,KAAK;IAAW,YAAY,CAAC,KAAK,KAAK;GAAc;EAC3E;CACF;;CAGA,MAAM,SAAS,MAA6B;EAC1C,MAAM,OAAO,EAAE,KAAK;EACpB,MAAM,SAAS,cAAc,IAAI;EACjC,KAAK,SAAS;EACd,IAAI;EACJ,IAAI;EACJ,IAAI,KAAK;EACT,IAAI;EACJ,IAAI;GAEF,OAAO,OAAO,QAAQ,MAAM,KAAK,MAAM,MAAM,EAAE,EAAA,EAAI,SAAS,aAAa,KAAK,QAAQ,EAAE;GACxF,IAAI,OAAO,QAAQ,SAAS,GAAG,SAAS,MAAM,EAAE;GAChD,MAAM,SAAqB,CAAC;GAC5B,WAAW,MAAM,SAAS,KAAK,MAAM,KAAK,EAAE,GAAG,OAAO,KAAK,KAAK;GAChE,UAAU;EACZ,SAAS,OAAO;GACd,UAAU,UAAU,KAAK;EAC3B;EACA,IAAI,SAAS,KAAK,OAAO;EACzB,KAAK,SAAS;EAEd,KADkB,KAAK,aAAa,EAAE,MAAM,GAAG,CAC5C,CAAC,CAAC,UAAU,EAAE,OAAO,QAAQ,QAAQ,IAAI,EAAE,QAAQ,CAAC;EACvD,KAAK,UAAU;EACf,KAAK,SAAS;EACd,KAAK,OAAO;CACd;CAEA,KAAoB;EAClB,OAAO,KAAK,SAAS,QAAQ,KAAK,MAAM,CAAC;CAC3C;CAEA,UAAyB;EACvB,OAAO,KAAK,SAAS,KAAK,MAAM;CAClC;CAEA,QAAQ,OAAoC;EAC1C,KAAK,KAAK,QAAQ,KAAK;EACvB,KAAK,OAAO;CACd;CAEA,aAAsB;EACpB,OAAO,WAAW,KAAK,KAAK,MAAM,KAAK,WAAW,CAAC;CACrD;CAEA,SAAS,OAAc;EACrB,OAAO,SAAS,KAAK,OAAO,KAAK;CACnC;CAEA,UAAU;EACR,KAAK;EACL,KAAK,cAAc;CACrB;AACF"}
@@ -0,0 +1,15 @@
1
+ import { FilesApi } from "@statewalker/webrun-files";
2
+ //#region src/paths.d.ts
3
+ export interface Crumb {
4
+ name: string;
5
+ path: string;
6
+ isMount: boolean;
7
+ }
8
+ export declare function breadcrumb(path: string, mountNames: Record<string, string>): Crumb[];
9
+ /** The folders beside a crumb: the ▾ menu. Beside a mount are the other mounts. */
10
+ export declare function siblings(files: FilesApi, crumb: Crumb): Promise<{
11
+ name: string;
12
+ path: string;
13
+ }[]>;
14
+ //#endregion
15
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","names":[],"sources":["../src/paths.ts"],"mappings":";;iBAGiB;EACf;EACA;EACA;;wBAGc,WAAW,cAAc,YAAY,yBAAyB;;wBAYxD,SACpB,OAAO,UACP,OAAO,QACN;EAAU;EAAc"}