@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.
Files changed (206) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +117 -0
  3. package/package.json +57 -0
  4. package/s/cellar/cask.ts +18 -0
  5. package/s/cellar/cellar.test.ts +100 -0
  6. package/s/cellar/cellar.ts +53 -0
  7. package/s/cellar/forklift.ts +9 -0
  8. package/s/cellar/index.ts +7 -0
  9. package/s/cellar/memory-forklift.ts +29 -0
  10. package/s/cellar/opfs-forklift.ts +49 -0
  11. package/s/demo/main.bundle.ts +39 -0
  12. package/s/demo/main.css +126 -0
  13. package/s/dom/components/breadcrumb/component.ts +29 -0
  14. package/s/dom/components/browser/component.ts +75 -0
  15. package/s/dom/components/browser/style.css.ts +118 -0
  16. package/s/dom/components/dropzone/component.ts +33 -0
  17. package/s/dom/components/dropzone/style.css.ts +54 -0
  18. package/s/dom/components/filter/component.ts +44 -0
  19. package/s/dom/components/outliner/component.ts +16 -0
  20. package/s/dom/components/searchbar/component.ts +29 -0
  21. package/s/dom/components/sort/component.ts +48 -0
  22. package/s/dom/components.ts +15 -0
  23. package/s/dom/context.ts +5 -0
  24. package/s/dom/theme.css.ts +11 -0
  25. package/s/dom/utils/find-group-name.ts +12 -0
  26. package/s/dom/utils/find-local-group.ts +9 -0
  27. package/s/dom/views/tree/component.ts +64 -0
  28. package/s/dom/views/tree/style.css.ts +64 -0
  29. package/s/genius/folio.md +29 -0
  30. package/s/genius/parts/hy-tree.ts +142 -0
  31. package/s/genius/quay.md +13 -0
  32. package/s/genius/turbo.md +21 -0
  33. package/s/genius/twig.ts +34 -0
  34. package/s/index.html.ts +89 -0
  35. package/s/index.ts +14 -0
  36. package/s/logic/aspects/codex/codex.ts +46 -0
  37. package/s/logic/aspects/codex/example.ts +65 -0
  38. package/s/logic/aspects/codex/parts/clade.ts +28 -0
  39. package/s/logic/aspects/codex/parts/codex-item.ts +94 -0
  40. package/s/logic/aspects/codex/parts/hierarchy.ts +82 -0
  41. package/s/logic/aspects/codex/parts/taxonomy.ts +18 -0
  42. package/s/logic/aspects/codex/parts/types.ts +23 -0
  43. package/s/logic/aspects/codex/utils/generate-id.ts +7 -0
  44. package/s/logic/aspects/dropzone.ts +77 -0
  45. package/s/logic/aspects/tree-trail.ts +33 -0
  46. package/s/logic/brain.ts +22 -0
  47. package/s/logic/group.ts +104 -0
  48. package/s/logic/permissions.ts +34 -0
  49. package/s/logic/presets/media/group.ts +94 -0
  50. package/s/logic/presets/media/schema.ts +22 -0
  51. package/s/logic/presets/opfs/schema.ts +21 -0
  52. package/s/logic/presets/plain/group.ts +44 -0
  53. package/s/logic/presets/plain/schema.ts +15 -0
  54. package/s/logic/types.ts +33 -0
  55. package/s/tests.test.ts +8 -0
  56. package/x/cellar/cask.d.ts +8 -0
  57. package/x/cellar/cask.js +19 -0
  58. package/x/cellar/cask.js.map +1 -0
  59. package/x/cellar/cellar.d.ts +22 -0
  60. package/x/cellar/cellar.js +47 -0
  61. package/x/cellar/cellar.js.map +1 -0
  62. package/x/cellar/cellar.test.d.ts +11 -0
  63. package/x/cellar/cellar.test.js +79 -0
  64. package/x/cellar/cellar.test.js.map +1 -0
  65. package/x/cellar/forklift.d.ts +7 -0
  66. package/x/cellar/forklift.js +2 -0
  67. package/x/cellar/forklift.js.map +1 -0
  68. package/x/cellar/index.d.ts +5 -0
  69. package/x/cellar/index.js +6 -0
  70. package/x/cellar/index.js.map +1 -0
  71. package/x/cellar/memory-forklift.d.ts +9 -0
  72. package/x/cellar/memory-forklift.js +21 -0
  73. package/x/cellar/memory-forklift.js.map +1 -0
  74. package/x/cellar/opfs-forklift.d.ts +11 -0
  75. package/x/cellar/opfs-forklift.js +44 -0
  76. package/x/cellar/opfs-forklift.js.map +1 -0
  77. package/x/demo/main.bundle.d.ts +1 -0
  78. package/x/demo/main.bundle.js +30 -0
  79. package/x/demo/main.bundle.js.map +1 -0
  80. package/x/demo/main.bundle.min.js +471 -0
  81. package/x/demo/main.css +126 -0
  82. package/x/dom/components/breadcrumb/component.d.ts +1 -0
  83. package/x/dom/components/breadcrumb/component.js +21 -0
  84. package/x/dom/components/breadcrumb/component.js.map +1 -0
  85. package/x/dom/components/browser/component.d.ts +1 -0
  86. package/x/dom/components/browser/component.js +65 -0
  87. package/x/dom/components/browser/component.js.map +1 -0
  88. package/x/dom/components/browser/style.css.d.ts +2 -0
  89. package/x/dom/components/browser/style.css.js +117 -0
  90. package/x/dom/components/browser/style.css.js.map +1 -0
  91. package/x/dom/components/dropzone/component.d.ts +1 -0
  92. package/x/dom/components/dropzone/component.js +27 -0
  93. package/x/dom/components/dropzone/component.js.map +1 -0
  94. package/x/dom/components/dropzone/style.css.d.ts +2 -0
  95. package/x/dom/components/dropzone/style.css.js +53 -0
  96. package/x/dom/components/dropzone/style.css.js.map +1 -0
  97. package/x/dom/components/filter/component.d.ts +1 -0
  98. package/x/dom/components/filter/component.js +40 -0
  99. package/x/dom/components/filter/component.js.map +1 -0
  100. package/x/dom/components/outliner/component.d.ts +1 -0
  101. package/x/dom/components/outliner/component.js +13 -0
  102. package/x/dom/components/outliner/component.js.map +1 -0
  103. package/x/dom/components/searchbar/component.d.ts +1 -0
  104. package/x/dom/components/searchbar/component.js +24 -0
  105. package/x/dom/components/searchbar/component.js.map +1 -0
  106. package/x/dom/components/sort/component.d.ts +1 -0
  107. package/x/dom/components/sort/component.js +44 -0
  108. package/x/dom/components/sort/component.js.map +1 -0
  109. package/x/dom/components.d.ts +18 -0
  110. package/x/dom/components.js +13 -0
  111. package/x/dom/components.js.map +1 -0
  112. package/x/dom/context.d.ts +2 -0
  113. package/x/dom/context.js +3 -0
  114. package/x/dom/context.js.map +1 -0
  115. package/x/dom/theme.css.d.ts +2 -0
  116. package/x/dom/theme.css.js +11 -0
  117. package/x/dom/theme.css.js.map +1 -0
  118. package/x/dom/utils/find-group-name.d.ts +1 -0
  119. package/x/dom/utils/find-group-name.js +9 -0
  120. package/x/dom/utils/find-group-name.js.map +1 -0
  121. package/x/dom/utils/find-local-group.d.ts +1 -0
  122. package/x/dom/utils/find-local-group.js +7 -0
  123. package/x/dom/utils/find-local-group.js.map +1 -0
  124. package/x/dom/views/tree/component.d.ts +7 -0
  125. package/x/dom/views/tree/component.js +52 -0
  126. package/x/dom/views/tree/component.js.map +1 -0
  127. package/x/dom/views/tree/style.css.d.ts +2 -0
  128. package/x/dom/views/tree/style.css.js +63 -0
  129. package/x/dom/views/tree/style.css.js.map +1 -0
  130. package/x/genius/folio.md +29 -0
  131. package/x/genius/parts/hy-tree.d.ts +44 -0
  132. package/x/genius/parts/hy-tree.js +116 -0
  133. package/x/genius/parts/hy-tree.js.map +1 -0
  134. package/x/genius/quay.md +13 -0
  135. package/x/genius/turbo.md +21 -0
  136. package/x/genius/twig.d.ts +34 -0
  137. package/x/genius/twig.js +2 -0
  138. package/x/genius/twig.js.map +1 -0
  139. package/x/importmap.json +47 -0
  140. package/x/index.d.ts +11 -0
  141. package/x/index.html +170 -0
  142. package/x/index.html.d.ts +3 -0
  143. package/x/index.html.js +87 -0
  144. package/x/index.html.js.map +1 -0
  145. package/x/index.js +9 -0
  146. package/x/index.js.map +1 -0
  147. package/x/logic/aspects/codex/codex.d.ts +17 -0
  148. package/x/logic/aspects/codex/codex.js +39 -0
  149. package/x/logic/aspects/codex/codex.js.map +1 -0
  150. package/x/logic/aspects/codex/example.d.ts +13 -0
  151. package/x/logic/aspects/codex/example.js +42 -0
  152. package/x/logic/aspects/codex/example.js.map +1 -0
  153. package/x/logic/aspects/codex/parts/clade.d.ts +16 -0
  154. package/x/logic/aspects/codex/parts/clade.js +23 -0
  155. package/x/logic/aspects/codex/parts/clade.js.map +1 -0
  156. package/x/logic/aspects/codex/parts/codex-item.d.ts +33 -0
  157. package/x/logic/aspects/codex/parts/codex-item.js +74 -0
  158. package/x/logic/aspects/codex/parts/codex-item.js.map +1 -0
  159. package/x/logic/aspects/codex/parts/hierarchy.d.ts +23 -0
  160. package/x/logic/aspects/codex/parts/hierarchy.js +69 -0
  161. package/x/logic/aspects/codex/parts/hierarchy.js.map +1 -0
  162. package/x/logic/aspects/codex/parts/taxonomy.d.ts +7 -0
  163. package/x/logic/aspects/codex/parts/taxonomy.js +13 -0
  164. package/x/logic/aspects/codex/parts/taxonomy.js.map +1 -0
  165. package/x/logic/aspects/codex/parts/types.d.ts +22 -0
  166. package/x/logic/aspects/codex/parts/types.js +2 -0
  167. package/x/logic/aspects/codex/parts/types.js.map +1 -0
  168. package/x/logic/aspects/codex/utils/generate-id.d.ts +1 -0
  169. package/x/logic/aspects/codex/utils/generate-id.js +5 -0
  170. package/x/logic/aspects/codex/utils/generate-id.js.map +1 -0
  171. package/x/logic/aspects/dropzone.d.ts +16 -0
  172. package/x/logic/aspects/dropzone.js +60 -0
  173. package/x/logic/aspects/dropzone.js.map +1 -0
  174. package/x/logic/aspects/tree-trail.d.ts +8 -0
  175. package/x/logic/aspects/tree-trail.js +24 -0
  176. package/x/logic/aspects/tree-trail.js.map +1 -0
  177. package/x/logic/brain.d.ts +11 -0
  178. package/x/logic/brain.js +17 -0
  179. package/x/logic/brain.js.map +1 -0
  180. package/x/logic/group.d.ts +51 -0
  181. package/x/logic/group.js +79 -0
  182. package/x/logic/group.js.map +1 -0
  183. package/x/logic/permissions.d.ts +18 -0
  184. package/x/logic/permissions.js +26 -0
  185. package/x/logic/permissions.js.map +1 -0
  186. package/x/logic/presets/media/group.d.ts +7 -0
  187. package/x/logic/presets/media/group.js +75 -0
  188. package/x/logic/presets/media/group.js.map +1 -0
  189. package/x/logic/presets/media/schema.d.ts +18 -0
  190. package/x/logic/presets/media/schema.js +2 -0
  191. package/x/logic/presets/media/schema.js.map +1 -0
  192. package/x/logic/presets/opfs/schema.d.ts +18 -0
  193. package/x/logic/presets/opfs/schema.js +2 -0
  194. package/x/logic/presets/opfs/schema.js.map +1 -0
  195. package/x/logic/presets/plain/group.d.ts +7 -0
  196. package/x/logic/presets/plain/group.js +33 -0
  197. package/x/logic/presets/plain/group.js.map +1 -0
  198. package/x/logic/presets/plain/schema.d.ts +12 -0
  199. package/x/logic/presets/plain/schema.js +2 -0
  200. package/x/logic/presets/plain/schema.js.map +1 -0
  201. package/x/logic/types.d.ts +20 -0
  202. package/x/logic/types.js +11 -0
  203. package/x/logic/types.js.map +1 -0
  204. package/x/tests.test.d.ts +1 -0
  205. package/x/tests.test.js +6 -0
  206. 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
+ }
@@ -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,9 @@
1
+
2
+ export type Forklift = {
3
+ list(): AsyncIterable<string>
4
+ has(label: string): Promise<boolean>
5
+ save(label: string, bytes: Uint8Array): Promise<void>
6
+ load(label: string): Promise<Uint8Array>
7
+ delete(label: string): Promise<void>
8
+ }
9
+
@@ -0,0 +1,7 @@
1
+
2
+ export * from "./cask.js"
3
+ export * from "./cellar.js"
4
+ export * from "./forklift.js"
5
+ export * from "./memory-forklift.js"
6
+ export * from "./opfs-forklift.js"
7
+
@@ -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
+
@@ -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
+