nodejs-fs-explorer-ui 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/README.md +52 -0
- package/dist/components/Editor.svelte +68 -0
- package/dist/components/Editor.svelte.d.ts +7 -0
- package/dist/components/Entry.svelte +159 -0
- package/dist/components/Entry.svelte.d.ts +10 -0
- package/dist/components/FolderContent.svelte +74 -0
- package/dist/components/FolderContent.svelte.d.ts +10 -0
- package/dist/components/Main.svelte +47 -0
- package/dist/components/Main.svelte.d.ts +10 -0
- package/dist/components/Toolbar.svelte +159 -0
- package/dist/components/Toolbar.svelte.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +10 -0
- package/dist/state.svelte.d.ts +6 -0
- package/dist/state.svelte.js +4 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# nodejs-fs-explorer-ui
|
|
2
|
+
|
|
3
|
+
File Explorer Svelte component for the [NodeJS](https://nodejs.org) [filesystem](https://nodejs.org/docs/latest/api/fs.html) API for using in the browser, i.e. for browser libraries that implement the NodeJS FS API such as [zenfs](https://github.com/zen-fs/) and [memfs](https://github.com/streamich/memfs).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install nodejs-fs-explorer-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script>
|
|
15
|
+
import FileExplorer from "nodejs-fs-explorer-ui";
|
|
16
|
+
import fs from "@zenfs/core";
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<FileExplorer {fs} />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Developing
|
|
23
|
+
|
|
24
|
+
Once you've installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Everything inside `src/lib` is part of the library, everything inside `src/routes` can be used as a showcase or preview app.
|
|
31
|
+
|
|
32
|
+
## Building
|
|
33
|
+
|
|
34
|
+
To build and pack:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm pack
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To create a production version of the library:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pnpm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can preview the production build with `pnpm run preview`.
|
|
47
|
+
|
|
48
|
+
## Publishing
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pnpm publish
|
|
52
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ArrowLeftIcon, SaveIcon } from "@lucide/svelte";
|
|
3
|
+
import { editorOpened } from "../state.svelte";
|
|
4
|
+
import type { NodeJsFs } from "../types.js";
|
|
5
|
+
import { onMount } from "svelte";
|
|
6
|
+
|
|
7
|
+
let { fs }: { fs: NodeJsFs } = $props();
|
|
8
|
+
let text = $state("");
|
|
9
|
+
let saving = $state(false);
|
|
10
|
+
|
|
11
|
+
onMount(async () => {
|
|
12
|
+
text = await fs.promises.readFile(editorOpened.path!, { encoding: "utf8" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
async function save() {
|
|
16
|
+
saving = true;
|
|
17
|
+
try {
|
|
18
|
+
await fs.promises.writeFile(editorOpened.path!, text);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log(err);
|
|
21
|
+
} finally {
|
|
22
|
+
saving = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function close() {
|
|
27
|
+
editorOpened.path = null;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="container">
|
|
32
|
+
<section role="toolbar">
|
|
33
|
+
<button onclick={close} title="Close" aria-label="Close">
|
|
34
|
+
<ArrowLeftIcon />
|
|
35
|
+
</button>
|
|
36
|
+
<button disabled={saving} onclick={save}>
|
|
37
|
+
<SaveIcon />
|
|
38
|
+
{#if saving}
|
|
39
|
+
Saving...
|
|
40
|
+
{:else}
|
|
41
|
+
Save
|
|
42
|
+
{/if}
|
|
43
|
+
</button>
|
|
44
|
+
</section>
|
|
45
|
+
<textarea aria-label="File contents" bind:value={text}></textarea>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
textarea {
|
|
50
|
+
flex: 1;
|
|
51
|
+
resize: none;
|
|
52
|
+
font-family: "Courier New", Courier, monospace;
|
|
53
|
+
padding: 10px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 5px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.container {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
height: 100%;
|
|
66
|
+
gap: 5px;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
DownloadIcon,
|
|
4
|
+
FileIcon,
|
|
5
|
+
FileSymlink,
|
|
6
|
+
FolderIcon,
|
|
7
|
+
MoreVerticalIcon,
|
|
8
|
+
TrashIcon,
|
|
9
|
+
XIcon,
|
|
10
|
+
} from "@lucide/svelte";
|
|
11
|
+
import { dirPath, editorOpened } from "../state.svelte";
|
|
12
|
+
import type { NodeJsFs } from "../types.js";
|
|
13
|
+
import type { Dirent } from "fs";
|
|
14
|
+
import { resolve } from "path-unified/posix";
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
fs,
|
|
18
|
+
entry,
|
|
19
|
+
loadDirData,
|
|
20
|
+
}: { fs: NodeJsFs; entry: Dirent; loadDirData: () => void } = $props();
|
|
21
|
+
// svelte-ignore state_referenced_locally
|
|
22
|
+
let path = resolve(dirPath.path!, entry.name);
|
|
23
|
+
|
|
24
|
+
let actionsDialog: HTMLDialogElement;
|
|
25
|
+
let deleteDialog: HTMLDialogElement;
|
|
26
|
+
|
|
27
|
+
function onclick() {
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
dirPath.path = path;
|
|
30
|
+
}
|
|
31
|
+
if (entry.isFile()) {
|
|
32
|
+
editorOpened.path = path;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function download() {
|
|
37
|
+
const file = new Blob([await fs.promises.readFile(path)]);
|
|
38
|
+
|
|
39
|
+
const link = document.createElement("a");
|
|
40
|
+
link.href = URL.createObjectURL(file);
|
|
41
|
+
link.download = entry.name;
|
|
42
|
+
|
|
43
|
+
document.body.appendChild(link);
|
|
44
|
+
link.click();
|
|
45
|
+
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
URL.revokeObjectURL(link.href);
|
|
48
|
+
document.body.removeChild(link);
|
|
49
|
+
}, 1000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function remove() {
|
|
53
|
+
try {
|
|
54
|
+
await fs.promises.unlink(path);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.log(err);
|
|
57
|
+
}
|
|
58
|
+
loadDirData();
|
|
59
|
+
deleteDialog.close();
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<dialog class="actions" bind:this={actionsDialog}>
|
|
64
|
+
<div class="container">
|
|
65
|
+
<div class="header">
|
|
66
|
+
<b>{entry.name}</b>
|
|
67
|
+
<button aria-label="Close" onclick={() => actionsDialog.close()}>
|
|
68
|
+
<XIcon />
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="buttons">
|
|
72
|
+
<button onclick={download}>
|
|
73
|
+
<DownloadIcon />
|
|
74
|
+
Download
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onclick={() => {
|
|
78
|
+
actionsDialog.close();
|
|
79
|
+
deleteDialog.showModal();
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<TrashIcon />
|
|
83
|
+
Delete
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</dialog>
|
|
88
|
+
|
|
89
|
+
<dialog class="delete-dialog" bind:this={deleteDialog}>
|
|
90
|
+
<div>
|
|
91
|
+
Are you sure you want to delete "{entry.name}" ?
|
|
92
|
+
</div>
|
|
93
|
+
<br />
|
|
94
|
+
<div style="display: flex; justify-content: space-between;">
|
|
95
|
+
<button onclick={() => deleteDialog.close()}>Cancel</button>
|
|
96
|
+
<button onclick={remove}>Delete</button>
|
|
97
|
+
</div>
|
|
98
|
+
</dialog>
|
|
99
|
+
|
|
100
|
+
<div class="container">
|
|
101
|
+
<button {onclick}>
|
|
102
|
+
{#if entry.isDirectory()}
|
|
103
|
+
<FolderIcon aria-label="Directory" />
|
|
104
|
+
{:else if entry.isFile()}
|
|
105
|
+
<FileIcon aria-label="File" />
|
|
106
|
+
{:else if entry.isSymbolicLink()}
|
|
107
|
+
<FileSymlink aria-label="Symlink" />
|
|
108
|
+
{/if}
|
|
109
|
+
{entry.name}
|
|
110
|
+
</button>
|
|
111
|
+
{#if entry.isFile()}
|
|
112
|
+
<button aria-label="More" onclick={() => actionsDialog.showModal()}>
|
|
113
|
+
<MoreVerticalIcon />
|
|
114
|
+
</button>
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<style>
|
|
119
|
+
.container {
|
|
120
|
+
display: grid;
|
|
121
|
+
grid-template-columns: 1fr auto;
|
|
122
|
+
gap: 5px;
|
|
123
|
+
}
|
|
124
|
+
button {
|
|
125
|
+
display: inline-flex;
|
|
126
|
+
gap: 5px;
|
|
127
|
+
align-items: center;
|
|
128
|
+
height: 30px;
|
|
129
|
+
}
|
|
130
|
+
dialog {
|
|
131
|
+
min-width: 300px;
|
|
132
|
+
min-height: 300px;
|
|
133
|
+
max-width: 500px;
|
|
134
|
+
padding: 5px;
|
|
135
|
+
border: 2px solid;
|
|
136
|
+
border-radius: 5px;
|
|
137
|
+
}
|
|
138
|
+
.actions .container,
|
|
139
|
+
.actions .buttons {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
gap: 5px;
|
|
143
|
+
}
|
|
144
|
+
.actions .header {
|
|
145
|
+
display: flex;
|
|
146
|
+
gap: 5px;
|
|
147
|
+
justify-content: space-between;
|
|
148
|
+
align-items: center;
|
|
149
|
+
}
|
|
150
|
+
.actions .buttons button {
|
|
151
|
+
width: 100%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.delete-dialog button {
|
|
155
|
+
width: 40%;
|
|
156
|
+
text-align: center;
|
|
157
|
+
display: inline-block;
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NodeJsFs } from "../types.ts";
|
|
2
|
+
import type { Dirent } from "fs";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
fs: NodeJsFs;
|
|
5
|
+
entry: Dirent;
|
|
6
|
+
loadDirData: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const Entry: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Entry = ReturnType<typeof Entry>;
|
|
10
|
+
export default Entry;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Entry from "./Entry.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import { addFiles } from "../lib.js";
|
|
5
|
+
import { dirPath } from "../state.svelte";
|
|
6
|
+
import type { NodeJsFs } from "../types.js";
|
|
7
|
+
import type { Dirent } from "fs";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
fs,
|
|
11
|
+
dirData,
|
|
12
|
+
loadDirData,
|
|
13
|
+
}: { fs: NodeJsFs; dirData: Dirent[]; loadDirData: () => void } = $props();
|
|
14
|
+
|
|
15
|
+
let dropZone: HTMLElement;
|
|
16
|
+
onMount(() => {
|
|
17
|
+
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
|
18
|
+
dropZone.addEventListener(eventName, preventDefault, false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function preventDefault(e: Event) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dropZone.addEventListener("drop", onDrop);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function onDrop(e: DragEvent) {
|
|
30
|
+
if (!e.dataTransfer) return;
|
|
31
|
+
const files = Array.from(e.dataTransfer.files);
|
|
32
|
+
if (!files.length) return;
|
|
33
|
+
let dir = dirPath.path!;
|
|
34
|
+
if (!dir.endsWith("/")) dir += "/";
|
|
35
|
+
try {
|
|
36
|
+
await addFiles(fs, dir, files);
|
|
37
|
+
loadDirData();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.log(err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<section
|
|
45
|
+
class="folder-content"
|
|
46
|
+
aria-label="Folder content"
|
|
47
|
+
bind:this={dropZone}
|
|
48
|
+
>
|
|
49
|
+
{#if dirData.length > 0}
|
|
50
|
+
{#each dirData as entry (entry.name)}
|
|
51
|
+
<Entry {fs} {entry} {loadDirData} />
|
|
52
|
+
{/each}
|
|
53
|
+
{:else}
|
|
54
|
+
<div class="empty-info">
|
|
55
|
+
<div>This folder is empty</div>
|
|
56
|
+
</div>
|
|
57
|
+
{/if}
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.folder-content {
|
|
62
|
+
height: 100%;
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
row-gap: 5px;
|
|
66
|
+
}
|
|
67
|
+
.empty-info {
|
|
68
|
+
height: 100%;
|
|
69
|
+
color: gray;
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
align-items: center;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NodeJsFs } from "../types.ts";
|
|
2
|
+
import type { Dirent } from "fs";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
fs: NodeJsFs;
|
|
5
|
+
dirData: Dirent[];
|
|
6
|
+
loadDirData: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const FolderContent: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type FolderContent = ReturnType<typeof FolderContent>;
|
|
10
|
+
export default FolderContent;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Toolbar from "./Toolbar.svelte";
|
|
3
|
+
import FolderContent from "./FolderContent.svelte";
|
|
4
|
+
import Editor from "./Editor.svelte";
|
|
5
|
+
import { dirPath, editorOpened } from "../state.svelte.js";
|
|
6
|
+
import type { NodeJsFs } from "../types.js";
|
|
7
|
+
import type { Dirent } from "fs";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
fs,
|
|
11
|
+
options = {},
|
|
12
|
+
}: { fs: NodeJsFs; options?: { initialDir?: string } } = $props();
|
|
13
|
+
// svelte-ignore state_referenced_locally
|
|
14
|
+
let initialDir = options.initialDir || "/";
|
|
15
|
+
dirPath.path = initialDir;
|
|
16
|
+
let dirDataPromise: Promise<Dirent[]> | undefined = $state.raw();
|
|
17
|
+
|
|
18
|
+
function loadDirData() {
|
|
19
|
+
dirDataPromise = fs.promises.readdir(dirPath.path!, {
|
|
20
|
+
withFileTypes: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
$effect(() => {
|
|
25
|
+
loadDirData();
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="container">
|
|
30
|
+
{#if editorOpened.path !== null}
|
|
31
|
+
<Editor {fs} />
|
|
32
|
+
{:else}
|
|
33
|
+
<Toolbar {fs} {loadDirData} />
|
|
34
|
+
{#if dirDataPromise}
|
|
35
|
+
{#await dirDataPromise then dirData}
|
|
36
|
+
<FolderContent {fs} {dirData} {loadDirData} />
|
|
37
|
+
{/await}
|
|
38
|
+
{/if}
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.container {
|
|
44
|
+
padding: 5px;
|
|
45
|
+
height: calc(100% - 10px);
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NodeJsFs } from "../types.ts";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
fs: NodeJsFs;
|
|
4
|
+
options?: {
|
|
5
|
+
initialDir?: string;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
declare const Main: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Main = ReturnType<typeof Main>;
|
|
10
|
+
export default Main;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ArrowLeftIcon,
|
|
4
|
+
CheckIcon,
|
|
5
|
+
FileUpIcon,
|
|
6
|
+
FolderPlusIcon,
|
|
7
|
+
XIcon,
|
|
8
|
+
} from "@lucide/svelte";
|
|
9
|
+
import { addFiles } from "../lib.js";
|
|
10
|
+
import { tick } from "svelte";
|
|
11
|
+
import { dirPath } from "../state.svelte";
|
|
12
|
+
import type { NodeJsFs } from "../types.js";
|
|
13
|
+
import { dirname, resolve } from "path-unified/posix";
|
|
14
|
+
|
|
15
|
+
let { fs, loadDirData }: { fs: NodeJsFs; loadDirData: () => void } = $props();
|
|
16
|
+
|
|
17
|
+
let newFolderNameEl: HTMLInputElement | undefined = $state(undefined);
|
|
18
|
+
let showNewFolderDiv = $state(false);
|
|
19
|
+
let folderName = $state("");
|
|
20
|
+
let canCreateFolder = $derived(folderName.trim() !== "");
|
|
21
|
+
|
|
22
|
+
let folderCreationError: null | string = $state(null);
|
|
23
|
+
|
|
24
|
+
function goBack() {
|
|
25
|
+
dirPath.path = dirname(dirPath.path!);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function importFiles() {
|
|
29
|
+
const fileInput = document.createElement("input");
|
|
30
|
+
fileInput.type = "file";
|
|
31
|
+
fileInput.hidden = true;
|
|
32
|
+
fileInput.multiple = true;
|
|
33
|
+
fileInput.onchange = async () => {
|
|
34
|
+
const files = Array.from(fileInput.files!);
|
|
35
|
+
if (files.length) {
|
|
36
|
+
let dir = dirPath.path!;
|
|
37
|
+
if (!dir.endsWith("/")) {
|
|
38
|
+
dir += "/";
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await addFiles(fs, dir, files);
|
|
42
|
+
loadDirData();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.log(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
fileInput.remove();
|
|
48
|
+
};
|
|
49
|
+
document.body.append(fileInput);
|
|
50
|
+
fileInput.click();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function createFolder() {
|
|
54
|
+
try {
|
|
55
|
+
await fs.promises.mkdir(resolve(dirPath.path!, folderName));
|
|
56
|
+
loadDirData();
|
|
57
|
+
folderCreationError = null;
|
|
58
|
+
folderName = "";
|
|
59
|
+
showNewFolderDiv = false;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
folderCreationError = String(err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<section role="toolbar" class="toolbar">
|
|
67
|
+
<button onclick={goBack} aria-label="Go back" title="Go back">
|
|
68
|
+
<ArrowLeftIcon />
|
|
69
|
+
</button>
|
|
70
|
+
<button onclick={importFiles} aria-label="Import files" title="Import files">
|
|
71
|
+
<FileUpIcon />
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
onclick={() => {
|
|
75
|
+
showNewFolderDiv = true;
|
|
76
|
+
tick().then(() => newFolderNameEl!.focus());
|
|
77
|
+
}}
|
|
78
|
+
aria-label="New folder"
|
|
79
|
+
title="New folder"
|
|
80
|
+
>
|
|
81
|
+
<FolderPlusIcon />
|
|
82
|
+
</button>
|
|
83
|
+
<input
|
|
84
|
+
type="text"
|
|
85
|
+
bind:value={dirPath.path}
|
|
86
|
+
disabled
|
|
87
|
+
aria-label="Current directory"
|
|
88
|
+
title="Current directory"
|
|
89
|
+
/>
|
|
90
|
+
</section>
|
|
91
|
+
{#if showNewFolderDiv}
|
|
92
|
+
<form
|
|
93
|
+
class="new-folder"
|
|
94
|
+
onsubmit={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
createFolder();
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
placeholder="Folder name"
|
|
102
|
+
required
|
|
103
|
+
bind:value={folderName}
|
|
104
|
+
bind:this={newFolderNameEl}
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
type="submit"
|
|
108
|
+
disabled={!canCreateFolder}
|
|
109
|
+
aria-label="Create folder"
|
|
110
|
+
title="Create folder"
|
|
111
|
+
>
|
|
112
|
+
<CheckIcon />
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onclick={() => {
|
|
117
|
+
showNewFolderDiv = false;
|
|
118
|
+
folderCreationError = null;
|
|
119
|
+
}}
|
|
120
|
+
aria-label="Cancel"
|
|
121
|
+
title="Cancel"
|
|
122
|
+
>
|
|
123
|
+
<XIcon />
|
|
124
|
+
</button>
|
|
125
|
+
</form>
|
|
126
|
+
<div role="alert">
|
|
127
|
+
{#if folderCreationError}
|
|
128
|
+
<!-- `key` ensures that the error is re-announced,
|
|
129
|
+
even if the error text is the same. -->
|
|
130
|
+
{#key folderCreationError}
|
|
131
|
+
<div class="error">
|
|
132
|
+
{folderCreationError}
|
|
133
|
+
</div>
|
|
134
|
+
{/key}
|
|
135
|
+
{/if}
|
|
136
|
+
</div>
|
|
137
|
+
{/if}
|
|
138
|
+
<hr />
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
.toolbar {
|
|
142
|
+
display: flex;
|
|
143
|
+
gap: 5px;
|
|
144
|
+
margin-bottom: 5px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.toolbar input {
|
|
148
|
+
flex-grow: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.new-folder {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 5px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.error {
|
|
157
|
+
color: red;
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NodeJsFs } from "../types.ts";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
fs: NodeJsFs;
|
|
4
|
+
loadDirData: () => void;
|
|
5
|
+
};
|
|
6
|
+
declare const Toolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Toolbar = ReturnType<typeof Toolbar>;
|
|
8
|
+
export default Toolbar;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/lib.d.ts
ADDED
package/dist/lib.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
async function addFile(fs, dir, file) {
|
|
2
|
+
const path = dir + file.name;
|
|
3
|
+
const uint8arr = new Uint8Array(await file.arrayBuffer());
|
|
4
|
+
await fs.promises.writeFile(path, uint8arr);
|
|
5
|
+
}
|
|
6
|
+
export async function addFiles(fs, dir, files) {
|
|
7
|
+
await Promise.all(files.map(async (file) => {
|
|
8
|
+
await addFile(fs, dir, file);
|
|
9
|
+
}));
|
|
10
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodejs-fs-explorer-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"!dist/**/*.test.*",
|
|
7
|
+
"!dist/**/*.spec.*"
|
|
8
|
+
],
|
|
9
|
+
"sideEffects": [
|
|
10
|
+
"**/*.css"
|
|
11
|
+
],
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"svelte": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"svelte": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/compat": "^2.0.2",
|
|
26
|
+
"@eslint/js": "^9.39.2",
|
|
27
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
28
|
+
"@sveltejs/kit": "^2.50.2",
|
|
29
|
+
"@sveltejs/package": "^2.5.7",
|
|
30
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
31
|
+
"@types/node": "^24",
|
|
32
|
+
"@zenfs/core": "^2.5.2",
|
|
33
|
+
"eslint": "^9.39.2",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"eslint-plugin-svelte": "^3.14.0",
|
|
36
|
+
"globals": "^17.3.0",
|
|
37
|
+
"prettier": "^3.8.1",
|
|
38
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
39
|
+
"publint": "^0.3.17",
|
|
40
|
+
"svelte": "^5.51.0",
|
|
41
|
+
"svelte-check": "^4.4.2",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"typescript-eslint": "^8.54.0",
|
|
44
|
+
"vite": "^7.3.1"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"svelte",
|
|
48
|
+
"nodejs",
|
|
49
|
+
"fs",
|
|
50
|
+
"filesystem",
|
|
51
|
+
"browser",
|
|
52
|
+
"explorer",
|
|
53
|
+
"ui"
|
|
54
|
+
],
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@lucide/svelte": "^0.577.0",
|
|
57
|
+
"path-unified": "^0.2.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"dev": "vite dev",
|
|
61
|
+
"build": "vite build && npm run prepack",
|
|
62
|
+
"preview": "vite preview",
|
|
63
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
64
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
65
|
+
"lint": "prettier --check . && eslint .",
|
|
66
|
+
"format": "prettier --write ."
|
|
67
|
+
}
|
|
68
|
+
}
|