@e280/quay 0.0.0-1
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 +23 -0
- package/README.md +117 -0
- package/package.json +57 -0
- package/s/cellar/cask.ts +18 -0
- package/s/cellar/cellar.test.ts +100 -0
- package/s/cellar/cellar.ts +53 -0
- package/s/cellar/forklift.ts +9 -0
- package/s/cellar/index.ts +7 -0
- package/s/cellar/memory-forklift.ts +29 -0
- package/s/cellar/opfs-forklift.ts +49 -0
- package/s/demo/main.bundle.ts +39 -0
- package/s/demo/main.css +126 -0
- package/s/dom/components/breadcrumb/component.ts +29 -0
- package/s/dom/components/browser/component.ts +75 -0
- package/s/dom/components/browser/style.css.ts +118 -0
- package/s/dom/components/dropzone/component.ts +33 -0
- package/s/dom/components/dropzone/style.css.ts +54 -0
- package/s/dom/components/filter/component.ts +44 -0
- package/s/dom/components/outliner/component.ts +16 -0
- package/s/dom/components/searchbar/component.ts +29 -0
- package/s/dom/components/sort/component.ts +48 -0
- package/s/dom/components.ts +15 -0
- package/s/dom/context.ts +5 -0
- package/s/dom/theme.css.ts +11 -0
- package/s/dom/utils/find-group-name.ts +12 -0
- package/s/dom/utils/find-local-group.ts +9 -0
- package/s/dom/views/tree/component.ts +64 -0
- package/s/dom/views/tree/style.css.ts +64 -0
- package/s/genius/folio.md +29 -0
- package/s/genius/parts/hy-tree.ts +142 -0
- package/s/genius/quay.md +13 -0
- package/s/genius/turbo.md +21 -0
- package/s/genius/twig.ts +34 -0
- package/s/index.html.ts +89 -0
- package/s/index.ts +14 -0
- package/s/logic/aspects/codex/codex.ts +46 -0
- package/s/logic/aspects/codex/example.ts +65 -0
- package/s/logic/aspects/codex/parts/clade.ts +28 -0
- package/s/logic/aspects/codex/parts/codex-item.ts +94 -0
- package/s/logic/aspects/codex/parts/hierarchy.ts +82 -0
- package/s/logic/aspects/codex/parts/taxonomy.ts +18 -0
- package/s/logic/aspects/codex/parts/types.ts +23 -0
- package/s/logic/aspects/codex/utils/generate-id.ts +7 -0
- package/s/logic/aspects/dropzone.ts +77 -0
- package/s/logic/aspects/tree-trail.ts +33 -0
- package/s/logic/brain.ts +22 -0
- package/s/logic/group.ts +104 -0
- package/s/logic/permissions.ts +34 -0
- package/s/logic/presets/media/group.ts +94 -0
- package/s/logic/presets/media/schema.ts +22 -0
- package/s/logic/presets/opfs/schema.ts +21 -0
- package/s/logic/presets/plain/group.ts +44 -0
- package/s/logic/presets/plain/schema.ts +15 -0
- package/s/logic/types.ts +33 -0
- package/s/tests.test.ts +8 -0
- package/x/cellar/cask.d.ts +8 -0
- package/x/cellar/cask.js +19 -0
- package/x/cellar/cask.js.map +1 -0
- package/x/cellar/cellar.d.ts +22 -0
- package/x/cellar/cellar.js +47 -0
- package/x/cellar/cellar.js.map +1 -0
- package/x/cellar/cellar.test.d.ts +11 -0
- package/x/cellar/cellar.test.js +79 -0
- package/x/cellar/cellar.test.js.map +1 -0
- package/x/cellar/forklift.d.ts +7 -0
- package/x/cellar/forklift.js +2 -0
- package/x/cellar/forklift.js.map +1 -0
- package/x/cellar/index.d.ts +5 -0
- package/x/cellar/index.js +6 -0
- package/x/cellar/index.js.map +1 -0
- package/x/cellar/memory-forklift.d.ts +9 -0
- package/x/cellar/memory-forklift.js +21 -0
- package/x/cellar/memory-forklift.js.map +1 -0
- package/x/cellar/opfs-forklift.d.ts +11 -0
- package/x/cellar/opfs-forklift.js +44 -0
- package/x/cellar/opfs-forklift.js.map +1 -0
- package/x/demo/main.bundle.d.ts +1 -0
- package/x/demo/main.bundle.js +30 -0
- package/x/demo/main.bundle.js.map +1 -0
- package/x/demo/main.bundle.min.js +471 -0
- package/x/demo/main.css +126 -0
- package/x/dom/components/breadcrumb/component.d.ts +1 -0
- package/x/dom/components/breadcrumb/component.js +21 -0
- package/x/dom/components/breadcrumb/component.js.map +1 -0
- package/x/dom/components/browser/component.d.ts +1 -0
- package/x/dom/components/browser/component.js +65 -0
- package/x/dom/components/browser/component.js.map +1 -0
- package/x/dom/components/browser/style.css.d.ts +2 -0
- package/x/dom/components/browser/style.css.js +117 -0
- package/x/dom/components/browser/style.css.js.map +1 -0
- package/x/dom/components/dropzone/component.d.ts +1 -0
- package/x/dom/components/dropzone/component.js +27 -0
- package/x/dom/components/dropzone/component.js.map +1 -0
- package/x/dom/components/dropzone/style.css.d.ts +2 -0
- package/x/dom/components/dropzone/style.css.js +53 -0
- package/x/dom/components/dropzone/style.css.js.map +1 -0
- package/x/dom/components/filter/component.d.ts +1 -0
- package/x/dom/components/filter/component.js +40 -0
- package/x/dom/components/filter/component.js.map +1 -0
- package/x/dom/components/outliner/component.d.ts +1 -0
- package/x/dom/components/outliner/component.js +13 -0
- package/x/dom/components/outliner/component.js.map +1 -0
- package/x/dom/components/searchbar/component.d.ts +1 -0
- package/x/dom/components/searchbar/component.js +24 -0
- package/x/dom/components/searchbar/component.js.map +1 -0
- package/x/dom/components/sort/component.d.ts +1 -0
- package/x/dom/components/sort/component.js +44 -0
- package/x/dom/components/sort/component.js.map +1 -0
- package/x/dom/components.d.ts +18 -0
- package/x/dom/components.js +13 -0
- package/x/dom/components.js.map +1 -0
- package/x/dom/context.d.ts +2 -0
- package/x/dom/context.js +3 -0
- package/x/dom/context.js.map +1 -0
- package/x/dom/theme.css.d.ts +2 -0
- package/x/dom/theme.css.js +11 -0
- package/x/dom/theme.css.js.map +1 -0
- package/x/dom/utils/find-group-name.d.ts +1 -0
- package/x/dom/utils/find-group-name.js +9 -0
- package/x/dom/utils/find-group-name.js.map +1 -0
- package/x/dom/utils/find-local-group.d.ts +1 -0
- package/x/dom/utils/find-local-group.js +7 -0
- package/x/dom/utils/find-local-group.js.map +1 -0
- package/x/dom/views/tree/component.d.ts +7 -0
- package/x/dom/views/tree/component.js +52 -0
- package/x/dom/views/tree/component.js.map +1 -0
- package/x/dom/views/tree/style.css.d.ts +2 -0
- package/x/dom/views/tree/style.css.js +63 -0
- package/x/dom/views/tree/style.css.js.map +1 -0
- package/x/genius/folio.md +29 -0
- package/x/genius/parts/hy-tree.d.ts +44 -0
- package/x/genius/parts/hy-tree.js +116 -0
- package/x/genius/parts/hy-tree.js.map +1 -0
- package/x/genius/quay.md +13 -0
- package/x/genius/turbo.md +21 -0
- package/x/genius/twig.d.ts +34 -0
- package/x/genius/twig.js +2 -0
- package/x/genius/twig.js.map +1 -0
- package/x/importmap.json +47 -0
- package/x/index.d.ts +11 -0
- package/x/index.html +170 -0
- package/x/index.html.d.ts +3 -0
- package/x/index.html.js +87 -0
- package/x/index.html.js.map +1 -0
- package/x/index.js +9 -0
- package/x/index.js.map +1 -0
- package/x/logic/aspects/codex/codex.d.ts +17 -0
- package/x/logic/aspects/codex/codex.js +39 -0
- package/x/logic/aspects/codex/codex.js.map +1 -0
- package/x/logic/aspects/codex/example.d.ts +13 -0
- package/x/logic/aspects/codex/example.js +42 -0
- package/x/logic/aspects/codex/example.js.map +1 -0
- package/x/logic/aspects/codex/parts/clade.d.ts +16 -0
- package/x/logic/aspects/codex/parts/clade.js +23 -0
- package/x/logic/aspects/codex/parts/clade.js.map +1 -0
- package/x/logic/aspects/codex/parts/codex-item.d.ts +33 -0
- package/x/logic/aspects/codex/parts/codex-item.js +74 -0
- package/x/logic/aspects/codex/parts/codex-item.js.map +1 -0
- package/x/logic/aspects/codex/parts/hierarchy.d.ts +23 -0
- package/x/logic/aspects/codex/parts/hierarchy.js +69 -0
- package/x/logic/aspects/codex/parts/hierarchy.js.map +1 -0
- package/x/logic/aspects/codex/parts/taxonomy.d.ts +7 -0
- package/x/logic/aspects/codex/parts/taxonomy.js +13 -0
- package/x/logic/aspects/codex/parts/taxonomy.js.map +1 -0
- package/x/logic/aspects/codex/parts/types.d.ts +22 -0
- package/x/logic/aspects/codex/parts/types.js +2 -0
- package/x/logic/aspects/codex/parts/types.js.map +1 -0
- package/x/logic/aspects/codex/utils/generate-id.d.ts +1 -0
- package/x/logic/aspects/codex/utils/generate-id.js +5 -0
- package/x/logic/aspects/codex/utils/generate-id.js.map +1 -0
- package/x/logic/aspects/dropzone.d.ts +16 -0
- package/x/logic/aspects/dropzone.js +60 -0
- package/x/logic/aspects/dropzone.js.map +1 -0
- package/x/logic/aspects/tree-trail.d.ts +8 -0
- package/x/logic/aspects/tree-trail.js +24 -0
- package/x/logic/aspects/tree-trail.js.map +1 -0
- package/x/logic/brain.d.ts +11 -0
- package/x/logic/brain.js +17 -0
- package/x/logic/brain.js.map +1 -0
- package/x/logic/group.d.ts +51 -0
- package/x/logic/group.js +79 -0
- package/x/logic/group.js.map +1 -0
- package/x/logic/permissions.d.ts +18 -0
- package/x/logic/permissions.js +26 -0
- package/x/logic/permissions.js.map +1 -0
- package/x/logic/presets/media/group.d.ts +7 -0
- package/x/logic/presets/media/group.js +75 -0
- package/x/logic/presets/media/group.js.map +1 -0
- package/x/logic/presets/media/schema.d.ts +18 -0
- package/x/logic/presets/media/schema.js +2 -0
- package/x/logic/presets/media/schema.js.map +1 -0
- package/x/logic/presets/opfs/schema.d.ts +18 -0
- package/x/logic/presets/opfs/schema.js +2 -0
- package/x/logic/presets/opfs/schema.js.map +1 -0
- package/x/logic/presets/plain/group.d.ts +7 -0
- package/x/logic/presets/plain/group.js +33 -0
- package/x/logic/presets/plain/group.js.map +1 -0
- package/x/logic/presets/plain/schema.d.ts +12 -0
- package/x/logic/presets/plain/schema.js +2 -0
- package/x/logic/presets/plain/schema.js.map +1 -0
- package/x/logic/types.d.ts +20 -0
- package/x/logic/types.js +11 -0
- package/x/logic/types.js.map +1 -0
- package/x/tests.test.d.ts +1 -0
- package/x/tests.test.js +6 -0
- package/x/tests.test.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 Przemysław Gałęzki
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
# 🏗️ Work In Progress
|
|
3
|
+
> This project is under construction. It's not finished.
|
|
4
|
+
> The install instructions probably won't work yet.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<br/><br/>
|
|
10
|
+
|
|
11
|
+
<div align="center"><img alt="" width=320 src="assets/favicon.png"/></div>
|
|
12
|
+
|
|
13
|
+
# 🪝 Quay
|
|
14
|
+
|
|
15
|
+
**Quay** is file-browser and outliner ui toolkit for the web. It provides a good user experience for interacting with files, folders, layers — any nested tree.
|
|
16
|
+
|
|
17
|
+
List-view or icons-view. Reorganize with drag-and-drop. Quick-search. Renaming things. Dropping files.
|
|
18
|
+
|
|
19
|
+
<br/>
|
|
20
|
+
|
|
21
|
+
## Quick-start OPFS file browser
|
|
22
|
+
|
|
23
|
+
Quay is a toolkit that can be used in many different ways.
|
|
24
|
+
In this section, several different intergration examples are provided.
|
|
25
|
+
|
|
26
|
+
### HTML technique
|
|
27
|
+
1. Install the script into your html `<head>`:
|
|
28
|
+
```html
|
|
29
|
+
<script type="module">
|
|
30
|
+
import * as Quay from "https://quay.e280.org/install.bundle.min.js"
|
|
31
|
+
const opfs = new Quay.Opfs({allow: ["/"]})
|
|
32
|
+
Quay.register(opfs.components())
|
|
33
|
+
</script>
|
|
34
|
+
```
|
|
35
|
+
1. Place the outliner component anywhere in your html `<body>`:
|
|
36
|
+
```html
|
|
37
|
+
<quay-outliner></quay-outliner>
|
|
38
|
+
```
|
|
39
|
+
1. Now you're done! Try it out, upload some files and play around.
|
|
40
|
+
|
|
41
|
+
### Web dev technique
|
|
42
|
+
1. Install the package into your project:
|
|
43
|
+
```sh
|
|
44
|
+
npm i @e280/quay
|
|
45
|
+
```
|
|
46
|
+
1. Put this code into your js/ts app entrypoint (like "main.ts" or something):
|
|
47
|
+
```ts
|
|
48
|
+
import * as Quay from "@e280/quay"
|
|
49
|
+
const opfs = new Quay.Opfs({allow: ["/"]})
|
|
50
|
+
Quay.register(opfs.components())
|
|
51
|
+
```
|
|
52
|
+
1. Place the outliner component anywhere in your html `<body>`:
|
|
53
|
+
```html
|
|
54
|
+
<quay-outliner></quay-outliner>
|
|
55
|
+
```
|
|
56
|
+
1. It's ready, take it for a spin!
|
|
57
|
+
|
|
58
|
+
<br/>
|
|
59
|
+
|
|
60
|
+
## Hands-on custom integration
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import * as Quay from "@e280/quay"
|
|
64
|
+
|
|
65
|
+
const file = Quay.make.file({label: "My cool file"})
|
|
66
|
+
const folder = Quay.make.folder({label: "My project", children: [file.id]})
|
|
67
|
+
|
|
68
|
+
const brain = new Quay.Brain({
|
|
69
|
+
state: new Quay.State({
|
|
70
|
+
rootId: folder.id,
|
|
71
|
+
items: [folder, file],
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
icons: Quay.icons.standard,
|
|
75
|
+
theme: Quay.themes.standard,
|
|
76
|
+
contextMenu: Quay.contextMenus.standard,
|
|
77
|
+
|
|
78
|
+
allowSearch: true,
|
|
79
|
+
allowRefresh: true,
|
|
80
|
+
permissions: async item => Quay.permissions.all,
|
|
81
|
+
|
|
82
|
+
actions: {
|
|
83
|
+
newFolder: async folder => {},
|
|
84
|
+
move: async move => {},
|
|
85
|
+
search: async search => {},
|
|
86
|
+
delete: async del => {},
|
|
87
|
+
rename: async rename => {},
|
|
88
|
+
refresh: async refresh => {},
|
|
89
|
+
dragAndDrop: async dnd => {},
|
|
90
|
+
upload: async upload => {},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
Quay.register(brain.components())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Flexible permissions
|
|
98
|
+
|
|
99
|
+
Let's say you wanted to make a particular folder read-only
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const restricted = Quay.make.folder({label: "Restricted"})
|
|
103
|
+
|
|
104
|
+
new Quay.Brain({
|
|
105
|
+
...stuff,
|
|
106
|
+
|
|
107
|
+
permissions: async item => {
|
|
108
|
+
if (item.id === restricted.id)
|
|
109
|
+
return Quay.permissions.readOnly
|
|
110
|
+
else
|
|
111
|
+
return Quay.permissions.all
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In this way, you can setup sophisticated rules about what actions are permitted, and under whatever changing circumstances.
|
|
117
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@e280/quay",
|
|
3
|
+
"version": "0.0.0-1",
|
|
4
|
+
"description": "File-browser and outliner UI for the web",
|
|
5
|
+
"author": "Przemysław Gałęzki",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "x/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"x",
|
|
11
|
+
"s"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@benev/slate": "^0.3.5",
|
|
15
|
+
"@e280/kv": "^0.0.0-7",
|
|
16
|
+
"@e280/stz": "^0.0.0-22"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@benev/turtle": "^0.6.13",
|
|
20
|
+
"@e280/science": "^0.0.5",
|
|
21
|
+
"@shoelace-style/shoelace": "^2.20.1",
|
|
22
|
+
"http-server": "^14.1.1",
|
|
23
|
+
"npm-run-all": "^4.1.5",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf x && mkdir x && run-s _code _ssg _ln",
|
|
28
|
+
"start": "run-p _http _turtlewatch",
|
|
29
|
+
"test": "node x/tests.test.js",
|
|
30
|
+
"test-watch": "node --watch x/tests.test.js",
|
|
31
|
+
"test-inspect": "node inspect x/tests.test.js",
|
|
32
|
+
"_code": "turtle build --out=x",
|
|
33
|
+
"_ssg": "turtle ssg --in=s,x --out=x",
|
|
34
|
+
"_http": "http-server x",
|
|
35
|
+
"_turtlewatch": "turtle watch --in=s,x --out=x -v",
|
|
36
|
+
"_ln": "run-s _ln:s _ln:assets",
|
|
37
|
+
"_ln:s": "ln -s \"$(realpath s)\" x/s",
|
|
38
|
+
"_ln:assets": "ln -s \"$(realpath assets)\" x/assets",
|
|
39
|
+
"count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"opfs",
|
|
43
|
+
"file browser",
|
|
44
|
+
"web components",
|
|
45
|
+
"outliner",
|
|
46
|
+
"drag and drop",
|
|
47
|
+
"web ui"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/e280/quay.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/e280/quay#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/e280/quay/issues"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/s/cellar/cask.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import {Hex} from "@e280/stz"
|
|
3
|
+
|
|
4
|
+
/** File with a hash label */
|
|
5
|
+
export class Cask {
|
|
6
|
+
static async hash(bytes: Uint8Array) {
|
|
7
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", bytes)
|
|
8
|
+
return Hex.fromBytes(new Uint8Array(hashBuffer))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static async make(bytes: Uint8Array) {
|
|
12
|
+
const hash = await this.hash(bytes)
|
|
13
|
+
return new this(hash, bytes)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(public hash: string, public bytes: Uint8Array) {}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
import {Txt} from "@e280/stz"
|
|
3
|
+
import {expect, Science, test} from "@e280/science"
|
|
4
|
+
|
|
5
|
+
import {Cask} from "./cask.js"
|
|
6
|
+
import {Cellar} from "./cellar.js"
|
|
7
|
+
import {MemoryForklift} from "./memory-forklift.js"
|
|
8
|
+
|
|
9
|
+
export default Science.suite({
|
|
10
|
+
"save + load roundtrip": test(async() => {
|
|
11
|
+
const cellar = new Cellar()
|
|
12
|
+
const text = "hello world"
|
|
13
|
+
const bytes = Txt.toBytes(text)
|
|
14
|
+
await cellar.save(bytes)
|
|
15
|
+
|
|
16
|
+
const hash = await Cask.hash(bytes)
|
|
17
|
+
expect(await cellar.has(hash)).is(true)
|
|
18
|
+
|
|
19
|
+
const loaded = await cellar.load(hash)
|
|
20
|
+
expect(loaded.hash).is(hash)
|
|
21
|
+
expect(Txt.fromBytes(loaded.bytes)).is(text)
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
"save is idempotent": test(async() => {
|
|
25
|
+
const cellar = new Cellar()
|
|
26
|
+
const bytes = new TextEncoder().encode("foo bar")
|
|
27
|
+
await cellar.save(bytes)
|
|
28
|
+
await cellar.save(bytes) // again
|
|
29
|
+
|
|
30
|
+
const hash = await Cask.hash(bytes)
|
|
31
|
+
expect(await cellar.has(hash)).is(true)
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
"throws on corruption": test(async() => {
|
|
35
|
+
const forklift = new MemoryForklift()
|
|
36
|
+
const cellar = new Cellar(forklift)
|
|
37
|
+
const good = new TextEncoder().encode("valid data")
|
|
38
|
+
const bad = new TextEncoder().encode("corrupt data")
|
|
39
|
+
|
|
40
|
+
const hash = await Cask.hash(good)
|
|
41
|
+
await forklift.save(hash, bad)
|
|
42
|
+
|
|
43
|
+
await expect(async() => cellar.load(hash)).throwsAsync()
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
"delete removes file": test(async() => {
|
|
47
|
+
const cellar = new Cellar()
|
|
48
|
+
const bytes = new TextEncoder().encode("temporary data")
|
|
49
|
+
await cellar.save(bytes)
|
|
50
|
+
|
|
51
|
+
const hash = await Cask.hash(bytes)
|
|
52
|
+
expect(await cellar.has(hash)).is(true)
|
|
53
|
+
|
|
54
|
+
await cellar.delete(hash)
|
|
55
|
+
expect(await cellar.has(hash)).is(false)
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
"load missing throws": test(async() => {
|
|
59
|
+
const cellar = new Cellar()
|
|
60
|
+
const fakeHash = "deadbeef1234567890"
|
|
61
|
+
|
|
62
|
+
await expect(async() => cellar.load(fakeHash)).throwsAsync()
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
"list returns saved hashes": test(async() => {
|
|
66
|
+
const cellar = new Cellar()
|
|
67
|
+
const texts = ["a", "b", "c"]
|
|
68
|
+
|
|
69
|
+
for (const t of texts)
|
|
70
|
+
await cellar.save(Txt.toBytes(t))
|
|
71
|
+
|
|
72
|
+
const hashes = []
|
|
73
|
+
for await (const hash of cellar.list())
|
|
74
|
+
hashes.push(hash)
|
|
75
|
+
|
|
76
|
+
expect(hashes.length).is(3)
|
|
77
|
+
for (const t of texts) {
|
|
78
|
+
const h = await Cask.hash(Txt.toBytes(t))
|
|
79
|
+
expect(hashes.includes(h)).is(true)
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
"clear removes all entries": test(async() => {
|
|
84
|
+
const cellar = new Cellar()
|
|
85
|
+
await cellar.save(Txt.toBytes("one"))
|
|
86
|
+
await cellar.save(Txt.toBytes("two"))
|
|
87
|
+
await cellar.save(Txt.toBytes("three"))
|
|
88
|
+
|
|
89
|
+
let count = 0
|
|
90
|
+
for await (const _ of cellar.list()) count++
|
|
91
|
+
expect(count).is(3)
|
|
92
|
+
|
|
93
|
+
await cellar.clear()
|
|
94
|
+
|
|
95
|
+
let postClearCount = 0
|
|
96
|
+
for await (const _ of cellar.list()) postClearCount++
|
|
97
|
+
expect(postClearCount).is(0)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import {Cask} from "./cask.js"
|
|
3
|
+
import {Forklift} from "./forklift.js"
|
|
4
|
+
import {OpfsForklift} from "./opfs-forklift.js"
|
|
5
|
+
import {MemoryForklift} from "./memory-forklift.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cellar is an immutable content-addressable file store
|
|
9
|
+
* - files are identified by their hashes (duplicates are impossible)
|
|
10
|
+
* - files cannot be modified (delete it and save another)
|
|
11
|
+
* - no filenames or metadata (store those somewhere else)
|
|
12
|
+
* - pass in a forklift, which is the backend that actually stores the data
|
|
13
|
+
* - MemoryForklift is the default
|
|
14
|
+
* - OpfsForklift is probably what you really want in the browser
|
|
15
|
+
*/
|
|
16
|
+
export class Cellar {
|
|
17
|
+
static async opfs(dirname = "cellar") {
|
|
18
|
+
return new this(await OpfsForklift.setup(dirname))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor(private forklift: Forklift = new MemoryForklift()) {}
|
|
22
|
+
|
|
23
|
+
async *list(): AsyncIterable<string> {
|
|
24
|
+
yield* this.forklift.list()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async has(hash: string): Promise<boolean> {
|
|
28
|
+
return this.forklift.has(hash)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async save(bytes: Uint8Array) {
|
|
32
|
+
const cask = await Cask.make(bytes)
|
|
33
|
+
if (!await this.forklift.has(cask.hash))
|
|
34
|
+
await this.forklift.save(cask.hash, cask.bytes)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async load(hash: string): Promise<Cask> {
|
|
38
|
+
const bytes = await this.forklift.load(hash)
|
|
39
|
+
const cask = await Cask.make(bytes)
|
|
40
|
+
if (cask.hash !== hash) throw new Error(`corruption error, hash mismatch (requested ${hash}) (got ${cask.hash})`)
|
|
41
|
+
return cask
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async delete(hash: string) {
|
|
45
|
+
await this.forklift.delete(hash)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async clear() {
|
|
49
|
+
for await (const hash of this.list())
|
|
50
|
+
await this.delete(hash)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Forklift} from "./forklift.js"
|
|
4
|
+
|
|
5
|
+
export class MemoryForklift implements Forklift {
|
|
6
|
+
#map = new MapG<string, Uint8Array>()
|
|
7
|
+
|
|
8
|
+
async *list(): AsyncIterable<string> {
|
|
9
|
+
for (const label of this.#map.keys())
|
|
10
|
+
yield label
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async has(label: string): Promise<boolean> {
|
|
14
|
+
return this.#map.has(label)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async save(label: string, bytes: Uint8Array): Promise<void> {
|
|
18
|
+
this.#map.set(label, bytes)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async load(label: string): Promise<Uint8Array> {
|
|
22
|
+
return this.#map.require(label)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async delete(label: string) {
|
|
26
|
+
this.#map.delete(label)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import {Forklift} from "./forklift.js"
|
|
3
|
+
|
|
4
|
+
export class OpfsForklift implements Forklift {
|
|
5
|
+
static async setup(dirname: string) {
|
|
6
|
+
const root = await navigator.storage.getDirectory()
|
|
7
|
+
const directory = await root.getDirectoryHandle(dirname, {create: true})
|
|
8
|
+
return new this(directory)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
constructor(private directory: FileSystemDirectoryHandle) {}
|
|
12
|
+
|
|
13
|
+
async *list(): AsyncIterable<string> {
|
|
14
|
+
for await (const [name, handle] of this.directory.entries()) {
|
|
15
|
+
if (handle.kind === "file")
|
|
16
|
+
yield name
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async has(label: string): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
await this.directory.getFileHandle(label)
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if ((err as DOMException).name === "NotFoundError") return false
|
|
27
|
+
throw err
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async save(label: string, bytes: Uint8Array): Promise<void> {
|
|
32
|
+
const fileHandle = await this.directory.getFileHandle(label, {create: true})
|
|
33
|
+
const writable = await fileHandle.createWritable()
|
|
34
|
+
await writable.write(bytes)
|
|
35
|
+
await writable.close()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async load(label: string): Promise<Uint8Array> {
|
|
39
|
+
const fileHandle = await this.directory.getFileHandle(label)
|
|
40
|
+
const file = await fileHandle.getFile()
|
|
41
|
+
const buffer = await file.arrayBuffer()
|
|
42
|
+
return new Uint8Array(buffer)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async delete(label: string) {
|
|
46
|
+
await this.directory.removeEntry(label)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
import {register} from "@benev/slate"
|
|
3
|
+
import {brain} from "../dom/context.js"
|
|
4
|
+
import {MediaGroup} from "../logic/presets/media/group.js"
|
|
5
|
+
import {MediaFormat} from "../logic/presets/media/schema.js"
|
|
6
|
+
import {components, setShoelaceDarkTheme} from "../dom/components.js"
|
|
7
|
+
|
|
8
|
+
const group = brain.setGroup("default", new MediaGroup())
|
|
9
|
+
const {codex, root} = group.config
|
|
10
|
+
|
|
11
|
+
const previewUrl = "/assets/preview.webp"
|
|
12
|
+
const introVid = codex.create("file", {format: "video", label: "01‑introduction.mp4", previewUrl})
|
|
13
|
+
const coverImg = codex.create("file", {format: "image", label: "cover.png", previewUrl})
|
|
14
|
+
const audioFx = codex.create("file", {format: "audio", label: "click.wav", previewUrl: null})
|
|
15
|
+
|
|
16
|
+
// sub folder
|
|
17
|
+
const sprites = codex.create("folder", {label: "sprites"})
|
|
18
|
+
const heroPng = codex.create("file", {format: "image", label: "hero.png", previewUrl})
|
|
19
|
+
const enemyPng = codex.create("file", {format: "image", label: "enemy.png", previewUrl})
|
|
20
|
+
|
|
21
|
+
codex.root(root)
|
|
22
|
+
.attach(introVid)
|
|
23
|
+
.attach(coverImg)
|
|
24
|
+
.attach(audioFx)
|
|
25
|
+
.attach(sprites)
|
|
26
|
+
|
|
27
|
+
sprites.attach(heroPng).attach(enemyPng)
|
|
28
|
+
|
|
29
|
+
group.on.upload.sub(({files, target}) => {
|
|
30
|
+
for(const file of files) {
|
|
31
|
+
const format = file.type.split("/")[0] as MediaFormat
|
|
32
|
+
const item = codex.create("file", {label: file.name, format, previewUrl})
|
|
33
|
+
target.attach(item)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
setShoelaceDarkTheme()
|
|
38
|
+
register(components)
|
|
39
|
+
|
package/s/demo/main.css
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
|
|
2
|
+
:root {
|
|
3
|
+
--quay: #eb6f1d;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
* {
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
html {
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
padding: 5vw;
|
|
16
|
+
|
|
17
|
+
font-family: sans-serif;
|
|
18
|
+
color: #fffa;
|
|
19
|
+
background: #071320;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 2em;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.title {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
gap: 0.5em;
|
|
33
|
+
|
|
34
|
+
> img {
|
|
35
|
+
width: 100%;
|
|
36
|
+
max-width: 5em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
> div {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
|
|
45
|
+
> h1 {
|
|
46
|
+
color: color-mix(in lch, var(--quay), white 20%);
|
|
47
|
+
text-shadow: 0 0 0.5em color-mix(in lch, transparent, var(--quay) 75%);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
> .version {
|
|
51
|
+
font-size: 0.8em;
|
|
52
|
+
opacity: 0.4;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.items {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 1em;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
quay-browser, quay-searchbar, quay-outliner, quay-breadcrumb, quay-dropzone {
|
|
64
|
+
padding: 1em;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
quay-browser {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
align-items: center;
|
|
71
|
+
width: 300px;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
quay-searchbar {
|
|
76
|
+
width: 100%;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
quay-dropzone {
|
|
80
|
+
height: 150px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
quay-outliner {
|
|
84
|
+
width: 300px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.components {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-wrap: wrap;
|
|
90
|
+
gap: 1em;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.component {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: start;
|
|
98
|
+
gap: 0.5em;
|
|
99
|
+
width: 300px;
|
|
100
|
+
padding: 1em;
|
|
101
|
+
|
|
102
|
+
h4 {
|
|
103
|
+
color: #ED701E;
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dropzone {
|
|
109
|
+
flex: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.searchbar {
|
|
113
|
+
flex: 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.outliner {
|
|
117
|
+
flex: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.breadcrumb {
|
|
121
|
+
flex: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.browser {
|
|
125
|
+
flex: 100%;
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import {html, shadowComponent} from "@benev/slate"
|
|
3
|
+
|
|
4
|
+
import themeCss from "../../theme.css.js"
|
|
5
|
+
|
|
6
|
+
import {findLocalGroup} from "../../utils/find-local-group.js"
|
|
7
|
+
import {CodexItem} from "../../../logic/aspects/codex/parts/codex-item.js"
|
|
8
|
+
|
|
9
|
+
export const QuayBreadcrumb = shadowComponent(use => {
|
|
10
|
+
use.css(themeCss)
|
|
11
|
+
|
|
12
|
+
const group = findLocalGroup(use.element)
|
|
13
|
+
const {trail, config: {codex, renderLabel}} = group
|
|
14
|
+
|
|
15
|
+
const click = (item: CodexItem) => (e: Event) => group.trail.setTrail(e, item)
|
|
16
|
+
|
|
17
|
+
return html`
|
|
18
|
+
<sl-breadcrumb>
|
|
19
|
+
${trail.signal.value.map(item => html`
|
|
20
|
+
<sl-breadcrumb-item @click="${click(item)}">
|
|
21
|
+
${item === null
|
|
22
|
+
? "root"
|
|
23
|
+
: renderLabel(codex.require(item.id))}
|
|
24
|
+
</sl-breadcrumb-item>
|
|
25
|
+
`)}
|
|
26
|
+
</sl-breadcrumb>
|
|
27
|
+
`
|
|
28
|
+
})
|
|
29
|
+
|