@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
@@ -0,0 +1,77 @@
1
+ import {ShockDragDrop, signal} from "@benev/slate"
2
+
3
+ import {Group} from "../group.js"
4
+ import {CodexItem} from "./codex/parts/codex-item.js"
5
+
6
+ export class Dropzone {
7
+ #drag_drop = new ShockDragDrop<CodexItem, CodexItem>({
8
+ handle_drop: (_e, grabbed, hovering) => this.group.move(grabbed, hovering)
9
+ })
10
+
11
+ constructor(private group: Group) {}
12
+
13
+ // to make drop file to import ui work
14
+ #hovering = signal<CodexItem | undefined>(undefined)
15
+
16
+ get grabbed() {
17
+ return this.#drag_drop.grabbed
18
+ }
19
+
20
+ get hovering() {
21
+ return this.#drag_drop.hovering ?? this.#hovering.value
22
+ }
23
+
24
+ dragenter = (e: DragEvent, target?: CodexItem) => {
25
+ e.preventDefault()
26
+ this.#drag_drop.dropzone.dragenter()(e)
27
+ this.#hovering.value = target
28
+ }
29
+
30
+ dragleave = (e: DragEvent) => {
31
+ this.#drag_drop.dropzone.dragleave()(e)
32
+ this.#hovering.value = undefined
33
+ }
34
+
35
+ dragstart = (e: DragEvent, n: CodexItem) => {
36
+ e.stopPropagation()
37
+ this.#drag_drop.dragzone.dragstart(n)(e)
38
+ }
39
+
40
+ dragover = (e: DragEvent, n: CodexItem) => {
41
+ e.preventDefault()
42
+ this.#drag_drop.dropzone.dragover(n)(e)
43
+ }
44
+
45
+ dragend = (e: DragEvent) => {
46
+ this.#drag_drop.dragzone.dragend()(e)
47
+ this.#hovering.value = undefined
48
+ }
49
+
50
+ drop = (e: DragEvent, target: CodexItem) => {
51
+ e.preventDefault()
52
+ const files = Array.from(e.dataTransfer?.files || [])
53
+
54
+ if (files.length) {
55
+ this.group.upload(files, target)
56
+ }
57
+
58
+ const same = this.grabbed?.id === this.hovering?.id
59
+ const child = this.grabbed?.children.some(c => this.hovering?.id === c.id)
60
+ const parent = this.grabbed?.parent?.id === this.hovering?.id
61
+
62
+ if(!same && !child && !parent)
63
+ this.#drag_drop.dropzone.drop(target)(e)
64
+
65
+ this.#hovering.value = undefined
66
+ }
67
+
68
+ change = (e: DragEvent, targetFolder: CodexItem) => {
69
+ const input = e.currentTarget as HTMLInputElement
70
+ const files = Array.from(input.files ?? [])
71
+
72
+ if (files.length) {
73
+ this.group.upload(files, targetFolder)
74
+ }
75
+ }
76
+ }
77
+
@@ -0,0 +1,33 @@
1
+ import {signal} from "@benev/slate"
2
+ import {Schema} from "./codex/parts/types.js"
3
+ import {CodexItem} from "./codex/parts/codex-item.js"
4
+
5
+ export class TreeTrail<Sc extends Schema> {
6
+ readonly signal = signal<CodexItem<Sc>[]>([])
7
+
8
+ constructor(root: CodexItem<Sc>) {
9
+ this.signal.value = [root]
10
+ }
11
+
12
+ setTrail(e: Event, item: CodexItem<Sc>) {
13
+ if (e.target !== e.currentTarget) return
14
+
15
+ const trail: CodexItem<Sc>[] = []
16
+
17
+ let cur = item.kind === "folder"
18
+ ? item
19
+ : item.parent
20
+
21
+ while (cur) {
22
+ trail.unshift(cur)
23
+ cur = cur.parent
24
+ }
25
+
26
+ this.signal.value = trail
27
+ }
28
+
29
+ get currentFolder() {
30
+ return this.signal.value.at(-1)?.children ?? []
31
+ }
32
+ }
33
+
@@ -0,0 +1,22 @@
1
+
2
+ import {MapG} from "@e280/stz"
3
+ import {Group} from "./group.js"
4
+
5
+ /**
6
+ * Quay's global state
7
+ * - manages groups
8
+ * - each group is a different hierarchy, which can have unique types and stuff
9
+ */
10
+ export class Brain {
11
+ #groups = new MapG<string, Group>()
12
+
13
+ setGroup<G extends Group>(name: string, group: G) {
14
+ this.#groups.set(name, group)
15
+ return group
16
+ }
17
+
18
+ getGroup<G extends Group>(name: string) {
19
+ return this.#groups.require(name) as G
20
+ }
21
+ }
22
+
@@ -0,0 +1,104 @@
1
+ import {sub} from "@e280/stz"
2
+ import {signal} from "@benev/slate"
3
+
4
+ import {Permission} from "./permissions.js"
5
+ import {Dropzone} from "./aspects/dropzone.js"
6
+ import {TreeTrail} from "./aspects/tree-trail.js"
7
+ import {Schema} from "./aspects/codex/parts/types.js"
8
+ import {GroupConfig, SearchFn, SortFn} from "./types.js"
9
+ import {CodexItem} from "./aspects/codex/parts/codex-item.js"
10
+
11
+ export class Group<Sc extends Schema = any> {
12
+ trail: TreeTrail<Sc>
13
+ dropzone = new Dropzone(this)
14
+ searchText = signal("")
15
+ selectedFilter = signal("")
16
+ selectedSort = signal("")
17
+
18
+ on = {
19
+ newFolder: sub<[{parent: CodexItem<Sc>}]>(),
20
+ move: sub<[{item: CodexItem<Sc>, target: CodexItem<Sc>}]>(),
21
+ delete: sub<[{item: CodexItem<Sc>}]>(),
22
+ rename: sub<[{item: CodexItem<Sc>, newName: string}]>(),
23
+ upload: sub<[{files: File[], target: CodexItem<Sc>}]>(),
24
+ search: sub<[{terms: string[]}]>(),
25
+ refresh: sub<[{}]>(),
26
+ }
27
+
28
+ constructor(public config: GroupConfig<Sc>) {
29
+ this.selectedFilter.value = config.defaultFilter
30
+ this.selectedSort.value = config.defaultSort
31
+ this.trail = new TreeTrail(config.root)
32
+ }
33
+
34
+ get permissions() {
35
+ return this.config.permissions as (item: CodexItem) => Permission
36
+ }
37
+
38
+ getFilterFn(filterKey: string): SearchFn<Sc> {
39
+ return this.config.filters.get(filterKey) ?? (() => true)
40
+ }
41
+
42
+ getSearchFn(searchText: string): SearchFn<Sc> {
43
+ const terms = searchText.trim().toLowerCase().split(/\s+/)
44
+ return item => this.config.search(terms, item)
45
+ }
46
+
47
+ getSortFn(sortKey: string): SortFn<Sc> {
48
+ return this.config.sorts?.get(sortKey) ?? (() => 0)
49
+ }
50
+
51
+ sort(items: CodexItem<Sc>[]): CodexItem<Sc>[] {
52
+ const sortFn = this.getSortFn(this.selectedSort.value)
53
+ return [...items].sort(sortFn)
54
+ }
55
+
56
+ matches(item: CodexItem<Sc>) {
57
+ const filterFn = this.getFilterFn(this.selectedFilter.value)
58
+ const searchFn = this.getSearchFn(this.searchText.value)
59
+ return filterFn(item) && searchFn(item)
60
+ }
61
+
62
+ move(item: CodexItem<Sc>, target: CodexItem<Sc>) {
63
+ if(!this.permissions(item).move)
64
+ throw new Error("move permission not granted")
65
+
66
+ item.detach()
67
+ target.attach(item)
68
+
69
+ this.on.move.pub({item, target})
70
+ }
71
+
72
+ delete(item: CodexItem<Sc>) {
73
+ if(!this.permissions(item).delete)
74
+ throw new Error("delete permission not granted")
75
+
76
+ item.destroy()
77
+
78
+ this.on.delete.pub({item})
79
+ }
80
+
81
+ rename(item: CodexItem<Sc>, newName: string) {
82
+ if(!this.permissions(item).rename)
83
+ throw new Error("rename permission not granted")
84
+
85
+ this.on.rename.pub({item, newName})
86
+ }
87
+
88
+ upload(files: File[], folder: CodexItem<Sc>) {
89
+ if(!this.permissions(folder).upload)
90
+ throw new Error("upload permission not granted")
91
+
92
+ this.on.upload.pub({files, target: folder})
93
+ }
94
+
95
+ addFolder(parent: CodexItem<Sc>) {
96
+ if(!this.permissions(parent).newFolder)
97
+ throw new Error("add folder permission not granted")
98
+
99
+ this.on.newFolder.pub({parent})
100
+ }
101
+
102
+ components = () => this.components
103
+ }
104
+
@@ -0,0 +1,34 @@
1
+ enum PermissionKey {
2
+ rename = "rename",
3
+ move = "move",
4
+ delete = "delete",
5
+ refresh = "refresh",
6
+ newFolder = "newFolder",
7
+ search = "search",
8
+ upload = "upload"
9
+ }
10
+
11
+ export type Permission = {
12
+ [K in PermissionKey]: boolean
13
+ }
14
+
15
+ const ALL_KEYS = Object.values(PermissionKey) as PermissionKey[]
16
+
17
+ function makePermissions(
18
+ defaultValue: boolean,
19
+ overrides: Partial<Permission> = {}
20
+ ): Permission {
21
+ const perms = {} as Permission
22
+ for (const key of ALL_KEYS)
23
+ perms[key] = key in overrides ? overrides[key]! : defaultValue
24
+ return perms
25
+ }
26
+
27
+ export const Permissions = {
28
+ all: makePermissions(true),
29
+ readOnly: makePermissions(false, {
30
+ [PermissionKey.refresh]: true,
31
+ [PermissionKey.search]: true,
32
+ }),
33
+ none: makePermissions(false),
34
+ }
@@ -0,0 +1,94 @@
1
+
2
+ import {MapG} from "@e280/stz"
3
+ import {Content, html} from "@benev/slate"
4
+
5
+ import Quay from "../../../index.js"
6
+ import {Group} from "../../group.js"
7
+ import {Codex} from "../../aspects/codex/codex.js"
8
+ import {MediaFormat, MediaSchema} from "./schema.js"
9
+ import {GroupConfig, SearchFn, SortFn} from "../../types.js"
10
+ import {CodexItem} from "../../aspects/codex/parts/codex-item.js"
11
+
12
+ export class MediaGroup extends Group<MediaSchema> implements Group {
13
+ static config = (): GroupConfig<MediaSchema> => {
14
+ const formatIcons = new MapG<MediaFormat, Content>()
15
+ .set("audio", html`<sl-icon name=music-note-beamed></sl-icon>`)
16
+ .set("video", html`<sl-icon name=film></sl-icon>`)
17
+ .set("image", html`<sl-icon name=image></sl-icon>`)
18
+ .set("other", html`<sl-icon name=file-earmark></sl-icon>`)
19
+
20
+ const filters = new Map<string, SearchFn<MediaSchema>>()
21
+ .set("all", () => true)
22
+ .set("video", item => item.isKind("folder") || (item.isKind("file") && item.specimen.format === "video"))
23
+ .set("audio", item => item.isKind("folder") || (item.isKind("file") && item.specimen.format === "audio"))
24
+
25
+ const sorts = new Map<string, SortFn<MediaSchema>>()
26
+ .set("label", (a, b) => a.specimen.label.localeCompare(b.specimen.label))
27
+ .set("format", (a, b) => {
28
+ if (a.isKind("file") && b.isKind("file"))
29
+ return a.specimen.format.localeCompare(b.specimen.format)
30
+
31
+ if (a.isKind("file")) return -1
32
+ if (b.isKind("file")) return 1
33
+
34
+ return 0
35
+ })
36
+
37
+ const search: (terms: string[], item: CodexItem<MediaSchema>) => boolean = (terms, item) => {
38
+ const query = terms.join(" ").trim().toLowerCase()
39
+ if (query === "") return true
40
+
41
+ if (item.kind === "folder") {
42
+ return item.children.some(child => search(terms, child))
43
+ }
44
+
45
+ return item.specimen.label.toLowerCase().includes(query)
46
+ }
47
+
48
+ const codex = Codex.setup<MediaSchema>({
49
+ folder: {icon: html`<sl-icon name="folder"></sl-icon>`},
50
+ file: {icon: html`<sl-icon name="file"></sl-icon>`},
51
+ })
52
+
53
+ const root = codex.root(
54
+ codex.create("folder", {label: "project"})
55
+ )
56
+
57
+ const renderIcon = (item: CodexItem<MediaSchema>, opened: boolean) => {
58
+ return item.isKind("file")
59
+ ? formatIcons.require(item.specimen.format)
60
+ : opened ? html`<sl-icon name="folder2-open"></sl-icon>` : item.taxon.icon
61
+ }
62
+
63
+ return {
64
+ codex,
65
+ root,
66
+ sorts,
67
+ filters: filters,
68
+ defaultFilter: "all",
69
+ defaultSort: "label",
70
+ search: (terms, item) => {
71
+ return terms.some(term => (
72
+ item.kind.includes(term) ||
73
+ item.id.includes(term) ||
74
+ item.specimen.label.includes(term)
75
+ )) || search(terms, item)
76
+ },
77
+ renderIcon,
78
+ renderLabel: item => item.specimen.label,
79
+ renderPreview: (item: CodexItem<MediaSchema>) => {
80
+ return (item.isKind("file") && item.specimen.previewUrl)
81
+ ? html`<img src=${item.specimen.previewUrl}>`
82
+ : renderIcon(item, false)
83
+ // // TODO icons
84
+ // : html`<sl-icon name=${item.taxon.icon}></sl-icon>`
85
+ },
86
+ permissions: item => Quay.permissions.all,
87
+ }
88
+ }
89
+
90
+ constructor() {
91
+ super(MediaGroup.config())
92
+ }
93
+ }
94
+
@@ -0,0 +1,22 @@
1
+
2
+ import {Content} from "@benev/slate"
3
+ import {AsSchema} from "../../aspects/codex/parts/types.js"
4
+
5
+ export type MediaFormat = "video" | "image" | "audio" | "other"
6
+
7
+ export type MediaSchema = AsSchema<{
8
+ taxon: {
9
+ icon: Content
10
+ }
11
+ specimens: {
12
+ folder: {
13
+ label: string
14
+ }
15
+ file: {
16
+ label: string
17
+ format: MediaFormat
18
+ previewUrl: string | null
19
+ }
20
+ }
21
+ }>
22
+
@@ -0,0 +1,21 @@
1
+
2
+ import {Content} from "@benev/slate"
3
+ import {AsSchema} from "../../aspects/codex/parts/types.js"
4
+
5
+ export type OpfsSchema = AsSchema<{
6
+ taxon: {
7
+ icon: Content
8
+ }
9
+ specimens: {
10
+ directory: {
11
+ label: string
12
+ }
13
+ file: {
14
+ label: string
15
+ size: string
16
+ type: string
17
+ lastModified: number
18
+ }
19
+ }
20
+ }>
21
+
@@ -0,0 +1,44 @@
1
+
2
+ import Quay from "../../../index.js"
3
+ import {Group} from "../../group.js"
4
+ import {PlainSchema} from "./schema.js"
5
+ import {GroupConfig, SortFn} from "../../types.js"
6
+ import {Codex} from "../../aspects/codex/codex.js"
7
+
8
+ export class PlainGroup extends Group<PlainSchema> {
9
+ static config = (): GroupConfig<PlainSchema> => {
10
+ const codex = Codex.setup<PlainSchema>({
11
+ item: {icon: "📄"},
12
+ })
13
+
14
+ const root = codex.root(
15
+ codex.create("item", {label: "root"})
16
+ )
17
+
18
+ return {
19
+ codex,
20
+ root,
21
+ defaultSort: "id",
22
+ sorts: new Map<string, SortFn<PlainSchema>>()
23
+ .set("label", (a, b) => a.id.localeCompare(b.id)),
24
+ defaultFilter: "all",
25
+ filters: new Map()
26
+ .set("all", () => true),
27
+ search: (terms, item) => {
28
+ return terms.some(term => (
29
+ item.kind.includes(term) ||
30
+ item.id.includes(term)
31
+ ))
32
+ },
33
+ renderLabel: item => item.id,
34
+ renderIcon: item => item.taxon.icon,
35
+ renderPreview: () => null,
36
+ permissions: item => Quay.permissions.all,
37
+ }
38
+ }
39
+
40
+ constructor() {
41
+ super(PlainGroup.config())
42
+ }
43
+ }
44
+
@@ -0,0 +1,15 @@
1
+
2
+ import {Content} from "@benev/slate"
3
+ import {AsSchema} from "../../aspects/codex/parts/types.js"
4
+
5
+ export type PlainSchema = AsSchema<{
6
+ taxon: {
7
+ icon: Content
8
+ }
9
+ specimens: {
10
+ item: {
11
+ label: string
12
+ }
13
+ }
14
+ }>
15
+
@@ -0,0 +1,33 @@
1
+
2
+ import {Permission} from "./permissions.js"
3
+ import {Codex} from "./aspects/codex/codex.js"
4
+ import {Schema} from "./aspects/codex/parts/types.js"
5
+ import {CodexItem} from "./aspects/codex/parts/codex-item.js"
6
+ import { Content } from "@benev/slate"
7
+
8
+ export type SearchFn<Sc extends Schema> = (item: CodexItem<Sc>) => boolean
9
+ export type SortFn<Sc extends Schema> = (a: CodexItem<Sc>, b: CodexItem<Sc>) => number
10
+
11
+ export interface GroupConfig<Sc extends Schema> {
12
+ codex: Codex<Sc>
13
+ root: CodexItem<Sc>
14
+ defaultFilter: string
15
+ filters: Map<string, SearchFn<Sc>>
16
+ defaultSort: string
17
+ sorts: Map<string, SortFn<Sc>>
18
+ search: (terms: string[], item: CodexItem<Sc>) => boolean
19
+ renderIcon: (item: CodexItem<Sc>, opened: boolean) => Content
20
+ renderLabel: (item: CodexItem<Sc>) => Content
21
+ renderPreview: (item: CodexItem<Sc>) => Content
22
+ permissions: (item: CodexItem<Sc>) => Permission
23
+ }
24
+
25
+ // export interface GroupActions<Sc extends Schema> {
26
+ // newFolder: (parent: CodexItem<Sc>) => Promise<void>
27
+ // move: (item: CodexItem<Sc>, target: CodexItem<Sc>) => Promise<void>
28
+ // delete: (item: CodexItem<Sc>) => Promise<void>
29
+ // rename: (item: CodexItem<Sc>, newName: string) => Promise<void>
30
+ // upload: (files: File[], target: CodexItem<Sc>) => Promise<void>
31
+ // search: (terms: string[]) => Promise<CodexItem<Sc>[]>
32
+ // refresh: () => Promise<void>
33
+ // }
@@ -0,0 +1,8 @@
1
+
2
+ import {Science} from "@e280/science"
3
+ import cellar from "./cellar/cellar.test.js"
4
+
5
+ await Science.run({
6
+ cellar,
7
+ })
8
+
@@ -0,0 +1,8 @@
1
+ /** File with a hash label */
2
+ export declare class Cask {
3
+ hash: string;
4
+ bytes: Uint8Array;
5
+ static hash(bytes: Uint8Array): Promise<string>;
6
+ static make(bytes: Uint8Array): Promise<Cask>;
7
+ constructor(hash: string, bytes: Uint8Array);
8
+ }
@@ -0,0 +1,19 @@
1
+ import { Hex } from "@e280/stz";
2
+ /** File with a hash label */
3
+ export class Cask {
4
+ hash;
5
+ bytes;
6
+ static async hash(bytes) {
7
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
8
+ return Hex.fromBytes(new Uint8Array(hashBuffer));
9
+ }
10
+ static async make(bytes) {
11
+ const hash = await this.hash(bytes);
12
+ return new this(hash, bytes);
13
+ }
14
+ constructor(hash, bytes) {
15
+ this.hash = hash;
16
+ this.bytes = bytes;
17
+ }
18
+ }
19
+ //# sourceMappingURL=cask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cask.js","sourceRoot":"","sources":["../../s/cellar/cask.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAE7B,6BAA6B;AAC7B,MAAM,OAAO,IAAI;IAWG;IAAqB;IAVxC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAiB;QAClC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAC/D,OAAO,GAAG,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;IACjD,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAiB;QAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC7B,CAAC;IAED,YAAmB,IAAY,EAAS,KAAiB;QAAtC,SAAI,GAAJ,IAAI,CAAQ;QAAS,UAAK,GAAL,KAAK,CAAY;IAAG,CAAC;CAC7D"}
@@ -0,0 +1,22 @@
1
+ import { Cask } from "./cask.js";
2
+ import { Forklift } from "./forklift.js";
3
+ /**
4
+ * Cellar is an immutable content-addressable file store
5
+ * - files are identified by their hashes (duplicates are impossible)
6
+ * - files cannot be modified (delete it and save another)
7
+ * - no filenames or metadata (store those somewhere else)
8
+ * - pass in a forklift, which is the backend that actually stores the data
9
+ * - MemoryForklift is the default
10
+ * - OpfsForklift is probably what you really want in the browser
11
+ */
12
+ export declare class Cellar {
13
+ private forklift;
14
+ static opfs(dirname?: string): Promise<Cellar>;
15
+ constructor(forklift?: Forklift);
16
+ list(): AsyncIterable<string>;
17
+ has(hash: string): Promise<boolean>;
18
+ save(bytes: Uint8Array): Promise<void>;
19
+ load(hash: string): Promise<Cask>;
20
+ delete(hash: string): Promise<void>;
21
+ clear(): Promise<void>;
22
+ }
@@ -0,0 +1,47 @@
1
+ import { Cask } from "./cask.js";
2
+ import { OpfsForklift } from "./opfs-forklift.js";
3
+ import { MemoryForklift } from "./memory-forklift.js";
4
+ /**
5
+ * Cellar is an immutable content-addressable file store
6
+ * - files are identified by their hashes (duplicates are impossible)
7
+ * - files cannot be modified (delete it and save another)
8
+ * - no filenames or metadata (store those somewhere else)
9
+ * - pass in a forklift, which is the backend that actually stores the data
10
+ * - MemoryForklift is the default
11
+ * - OpfsForklift is probably what you really want in the browser
12
+ */
13
+ export class Cellar {
14
+ forklift;
15
+ static async opfs(dirname = "cellar") {
16
+ return new this(await OpfsForklift.setup(dirname));
17
+ }
18
+ constructor(forklift = new MemoryForklift()) {
19
+ this.forklift = forklift;
20
+ }
21
+ async *list() {
22
+ yield* this.forklift.list();
23
+ }
24
+ async has(hash) {
25
+ return this.forklift.has(hash);
26
+ }
27
+ async save(bytes) {
28
+ const cask = await Cask.make(bytes);
29
+ if (!await this.forklift.has(cask.hash))
30
+ await this.forklift.save(cask.hash, cask.bytes);
31
+ }
32
+ async load(hash) {
33
+ const bytes = await this.forklift.load(hash);
34
+ const cask = await Cask.make(bytes);
35
+ if (cask.hash !== hash)
36
+ throw new Error(`corruption error, hash mismatch (requested ${hash}) (got ${cask.hash})`);
37
+ return cask;
38
+ }
39
+ async delete(hash) {
40
+ await this.forklift.delete(hash);
41
+ }
42
+ async clear() {
43
+ for await (const hash of this.list())
44
+ await this.delete(hash);
45
+ }
46
+ }
47
+ //# sourceMappingURL=cellar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cellar.js","sourceRoot":"","sources":["../../s/cellar/cellar.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAE9B,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAA;AAEnD;;;;;;;;GAQG;AACH,MAAM,OAAO,MAAM;IAKE;IAJpB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,QAAQ;QACnC,OAAO,IAAI,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;IACnD,CAAC;IAED,YAAoB,WAAqB,IAAI,cAAc,EAAE;QAAzC,aAAQ,GAAR,QAAQ,CAAiC;IAAG,CAAC;IAEjE,KAAK,CAAC,CAAC,IAAI;QACV,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiB;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YACtC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACjD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,UAAU,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;QACjH,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACxB,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;YACnC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;CACD"}
@@ -0,0 +1,11 @@
1
+ import { Science } from "@e280/science";
2
+ declare const _default: {
3
+ "save + load roundtrip": Science.Test;
4
+ "save is idempotent": Science.Test;
5
+ "throws on corruption": Science.Test;
6
+ "delete removes file": Science.Test;
7
+ "load missing throws": Science.Test;
8
+ "list returns saved hashes": Science.Test;
9
+ "clear removes all entries": Science.Test;
10
+ };
11
+ export default _default;