@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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
import {Kv, collect} from "@e280/kv"
|
|
3
|
+
|
|
4
|
+
export type HyId = string
|
|
5
|
+
export type HyItem = {parent: HyId | null, children: HyId[]}
|
|
6
|
+
type HyRecord = [parent: HyId | null, children: HyId[]]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Async hierarchy of nestable items.
|
|
10
|
+
* - items can have multiple children.
|
|
11
|
+
* - items can only have one parent.
|
|
12
|
+
* - items can be orphans (bring your own concept of a "root").
|
|
13
|
+
*/
|
|
14
|
+
export class HyTree {
|
|
15
|
+
constructor(private records: Kv<HyRecord>) {}
|
|
16
|
+
|
|
17
|
+
async has(id: HyId) {
|
|
18
|
+
return this.records.has(id)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async get(id: HyId): Promise<HyItem | undefined> {
|
|
22
|
+
const record = await this.records.get(id)
|
|
23
|
+
if (record) {
|
|
24
|
+
const [parent, children] = record
|
|
25
|
+
return {parent, children}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async gets(...ids: HyId[]): Promise<(HyItem | undefined)[]> {
|
|
30
|
+
const records = await this.records.gets(...ids)
|
|
31
|
+
return records.map(r => {
|
|
32
|
+
if (!r) return undefined
|
|
33
|
+
const [parent, children] = r
|
|
34
|
+
return {parent, children}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async require(id: HyId): Promise<HyItem> {
|
|
39
|
+
const record = await this.records.require(id)
|
|
40
|
+
const [parent, children] = record
|
|
41
|
+
return {parent, children}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async requires(...ids: HyId[]): Promise<HyItem[]> {
|
|
45
|
+
const records = await this.records.requires(...ids)
|
|
46
|
+
return records.map(([parent, children]) => ({parent, children}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** establish an id as a root with no parents, but ready to accept children */
|
|
50
|
+
async establishRoot(root: HyId) {
|
|
51
|
+
return this.records.set(root, [null, []])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** give a parent some children */
|
|
55
|
+
async attach(parent: HyId, ...children: HyId[]) {
|
|
56
|
+
children = [...new Set(children)]
|
|
57
|
+
const parentItem = await this.require(parent)
|
|
58
|
+
const siblings = new Set(parentItem.children)
|
|
59
|
+
const childItems = await this.gets(...children)
|
|
60
|
+
const newcomers: [HyId, HyRecord][] = []
|
|
61
|
+
|
|
62
|
+
for (const [index, id] of children.entries()) {
|
|
63
|
+
const item = childItems.at(index)
|
|
64
|
+
|
|
65
|
+
// child already in tree
|
|
66
|
+
if (item && item.parent) throw new Error(`child already has parent`)
|
|
67
|
+
|
|
68
|
+
// child not in tree yet, remember to save it
|
|
69
|
+
else newcomers.push([id, [parent, []]])
|
|
70
|
+
|
|
71
|
+
// add to siblings set
|
|
72
|
+
siblings.add(id)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// save all new children
|
|
76
|
+
await this.records.sets(...newcomers)
|
|
77
|
+
|
|
78
|
+
// save updated parent item
|
|
79
|
+
await this.records.set(parent, [parentItem.parent, [...siblings]])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** detach this id from its parent in the hierarchy */
|
|
83
|
+
async detach(id: HyId) {
|
|
84
|
+
const item = await this.get(id)
|
|
85
|
+
if (!item) return undefined
|
|
86
|
+
|
|
87
|
+
if (item.parent) {
|
|
88
|
+
const parentItem = await this.require(item.parent)
|
|
89
|
+
const siblings = new Set(parentItem.children)
|
|
90
|
+
siblings.delete(id)
|
|
91
|
+
|
|
92
|
+
// save the parent item with removed child
|
|
93
|
+
await this.records.set(item.parent, [parentItem.parent, [...siblings]])
|
|
94
|
+
|
|
95
|
+
// save the child with null parent
|
|
96
|
+
await this.records.set(id, [null, item.children])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** destroy all relations associated with this id, and all its descendants */
|
|
101
|
+
async destroy(id: HyId) {
|
|
102
|
+
await this.detach(id)
|
|
103
|
+
const tree = await collect(this.crawl(id))
|
|
104
|
+
const ids = tree.map(({id}) => id)
|
|
105
|
+
await this.records.del(...ids)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** delete absolutely everything */
|
|
109
|
+
async clear() {
|
|
110
|
+
await this.records.clear()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** iterate over all root items */
|
|
114
|
+
async *roots() {
|
|
115
|
+
for await (const [id, [parent, children]] of this.records.entries())
|
|
116
|
+
if (parent === null)
|
|
117
|
+
yield {id, item: {parent, children}} as {id: HyId, item: HyItem}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** iterate over this id and all its descendants */
|
|
121
|
+
async *crawl(
|
|
122
|
+
id: HyId,
|
|
123
|
+
predicate: (id: HyId, path: HyId[]) => Promise<boolean> = async() => true,
|
|
124
|
+
) {
|
|
125
|
+
|
|
126
|
+
const todo: [item: HyId, path: HyId[]][] = [[id, []]]
|
|
127
|
+
const seen = new Set<HyId>()
|
|
128
|
+
|
|
129
|
+
while (todo.length) {
|
|
130
|
+
const [id, path] = todo.shift()!
|
|
131
|
+
if (seen.has(id) || !await predicate(id, path)) continue
|
|
132
|
+
seen.add(id)
|
|
133
|
+
|
|
134
|
+
const item = await this.require(id)
|
|
135
|
+
yield {id, item, path} as {id: HyId, item: HyItem, path: HyId[]}
|
|
136
|
+
|
|
137
|
+
for (const childId of item.children)
|
|
138
|
+
todo.push([childId, [...path, id]])
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
package/s/genius/quay.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# ⚡ Turbo
|
|
3
|
+
|
|
4
|
+
**Turbo** is a turbocharger for reading and writing files in [OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system).
|
|
5
|
+
|
|
6
|
+
It provides high-performance multithreaded low-level read/write access by automatically scheduling parallel tasks across a pool of web workers.
|
|
7
|
+
|
|
8
|
+
- 📂 **OPFS** — files stay local on the user's device
|
|
9
|
+
- 🧵 **Multithreaded** — goes fast with no ui jank
|
|
10
|
+
- ⚙️ **Low-level** — streaming byte-level access
|
|
11
|
+
- 🔒 **Smart** — prevents funky race conditions
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
> ### Wip: implementation plan
|
|
18
|
+
> - use comrade for worker pooling
|
|
19
|
+
> - workers use opfs createSyncAccessHandle, make TransformStream readable/writable pair, wire it to the file stream, transfer the other end to the main thread, voila, connected
|
|
20
|
+
> - we have to do some big-brain horseshit to schedule tasks in a way to maximize parallelism while respecting ongoing locks
|
|
21
|
+
|
package/s/genius/twig.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 🌿 Async source for loading items into Quay.
|
|
4
|
+
* - crawlable and callable by the ui
|
|
5
|
+
* - the functions provided are available for the ui user
|
|
6
|
+
* - as we crawl, every item we see is cached for sync rendering
|
|
7
|
+
* - minimal so as to be compatible even with raw opfs
|
|
8
|
+
* - Opfs, Cellar, etc, will literally provide Twig implementations
|
|
9
|
+
*/
|
|
10
|
+
export type Twig<T extends Twig = any> = {
|
|
11
|
+
readonly id: string
|
|
12
|
+
name: string
|
|
13
|
+
parent: T | null
|
|
14
|
+
|
|
15
|
+
rename?: (name: string) => Promise<void>
|
|
16
|
+
delete?: () => Promise<void>
|
|
17
|
+
move?: (o: {parent: T}) => Promise<void>
|
|
18
|
+
copy?: (o: {parent: T, name: string}) => Promise<void>
|
|
19
|
+
|
|
20
|
+
container?: {
|
|
21
|
+
create(o: CreationOptions): Promise<T>
|
|
22
|
+
children(o?: ListingOptions): Promise<T[]>
|
|
23
|
+
crawl(predicate: (item: T, ancestors: T[]) => Promise<boolean>): AsyncIterable<T>
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Lens<T extends Twig = any> = (item: T) => boolean
|
|
28
|
+
export type WritingOptions = {}
|
|
29
|
+
export type ReadingOptions = {}
|
|
30
|
+
export type CreationOptions = {}
|
|
31
|
+
export type ListingOptions<T extends Twig = any> = {
|
|
32
|
+
lens?: Lens<T>
|
|
33
|
+
}
|
|
34
|
+
|
package/s/index.html.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import "@benev/slate/x/node.js"
|
|
2
|
+
import {template, html, easypage, headScripts, git_commit_hash, read_file, renderSocialCard, read_json} from "@benev/turtle"
|
|
3
|
+
|
|
4
|
+
const domain = "quay.e280.org"
|
|
5
|
+
const favicon = "/assets/favicon.png"
|
|
6
|
+
|
|
7
|
+
export default template(async basic => {
|
|
8
|
+
const path = basic.path(import.meta.url)
|
|
9
|
+
const hash = await git_commit_hash()
|
|
10
|
+
const version = (await read_json("package.json")).version as string
|
|
11
|
+
|
|
12
|
+
return easypage({
|
|
13
|
+
path,
|
|
14
|
+
dark: true,
|
|
15
|
+
title: "Quay",
|
|
16
|
+
css: "demo/main.css",
|
|
17
|
+
head: html`
|
|
18
|
+
<link rel="icon" href="${favicon}"/>
|
|
19
|
+
<meta data-commit-hash="${hash}"/>
|
|
20
|
+
|
|
21
|
+
${renderSocialCard({
|
|
22
|
+
themeColor: "#eb6f1d",
|
|
23
|
+
siteName: domain,
|
|
24
|
+
title: "Quay — File-browser UI Framework",
|
|
25
|
+
description: "Customizable outliner web ui toolkit for nested things",
|
|
26
|
+
image: `https://${domain}${favicon}`,
|
|
27
|
+
url: `https://${domain}/`,
|
|
28
|
+
})}
|
|
29
|
+
|
|
30
|
+
${headScripts({
|
|
31
|
+
devModulePath: await path.version.root("demo/main.bundle.js"),
|
|
32
|
+
prodModulePath: await path.version.root("demo/main.bundle.min.js"),
|
|
33
|
+
importmapContent: await read_file("x/importmap.json"),
|
|
34
|
+
})}
|
|
35
|
+
|
|
36
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/themes/dark.css" />
|
|
37
|
+
<!-- loading every sholeace component for now (should be cherry picked instead) --!>
|
|
38
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/shoelace.js" ></script>
|
|
39
|
+
`,
|
|
40
|
+
body: html`
|
|
41
|
+
<header class=title>
|
|
42
|
+
<img alt="" src="/assets/favicon.png"/>
|
|
43
|
+
<div>
|
|
44
|
+
<h1>Quay</h1>
|
|
45
|
+
<span class=version>v${version}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<div class=components>
|
|
50
|
+
|
|
51
|
+
<div class="component dropzone">
|
|
52
|
+
<h4>quay-dropzone</h2>
|
|
53
|
+
<quay-dropzone></quay-dropzone>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="component outliner">
|
|
57
|
+
<h4>quay-outliner</h2>
|
|
58
|
+
<quay-outliner></quay-outliner>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="component searchbar">
|
|
62
|
+
<h4>quay-searchbar</h2>
|
|
63
|
+
<quay-searchbar></quay-searchbar>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="component breadcrumb">
|
|
67
|
+
<h4>quay-breadcrumb</h2>
|
|
68
|
+
<quay-breadcrumb></quay-breadcrumb>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="component filter">
|
|
72
|
+
<h4>quay-filter</h2>
|
|
73
|
+
<quay-filter></quay-filter>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="component sort">
|
|
77
|
+
<h4>quay-sort</h2>
|
|
78
|
+
<quay-sort></quay-sort>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="component browser">
|
|
82
|
+
<h4>quay-browser</h2>
|
|
83
|
+
<quay-browser></quay-browser>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
`,
|
|
88
|
+
})
|
|
89
|
+
})
|
package/s/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
export {register, css} from "@benev/slate"
|
|
3
|
+
|
|
4
|
+
export * from "./cellar/index.js"
|
|
5
|
+
|
|
6
|
+
export * from "./dom/components.js"
|
|
7
|
+
import {Permissions} from "./logic/permissions.js"
|
|
8
|
+
|
|
9
|
+
const Quay = {
|
|
10
|
+
permissions: Permissions,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Quay
|
|
14
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
|
|
4
|
+
import {Clade} from "./parts/clade.js"
|
|
5
|
+
import {Taxonomy} from "./parts/taxonomy.js"
|
|
6
|
+
import {Hierarchy} from "./parts/hierarchy.js"
|
|
7
|
+
import {CodexItem} from "./parts/codex-item.js"
|
|
8
|
+
import {generateId} from "./utils/generate-id.js"
|
|
9
|
+
import {Id, Schema, Taxons} from "./parts/types.js"
|
|
10
|
+
|
|
11
|
+
export class Codex<Sc extends Schema> {
|
|
12
|
+
static setup<Sc extends Schema>(taxons: Taxons<Sc>) {
|
|
13
|
+
const taxonomy = new Taxonomy<Sc>(taxons)
|
|
14
|
+
const clade = new Clade(taxonomy)
|
|
15
|
+
const hierarchy = new Hierarchy()
|
|
16
|
+
return new this(clade, hierarchy)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#items = new MapG<Id, CodexItem<Sc>>()
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
public clade: Clade<Sc>,
|
|
23
|
+
public hierarchy: Hierarchy,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/** create an item, not yet added to hierarchy */
|
|
27
|
+
create<K extends keyof Sc["specimens"]>(kind: K, specimen: Sc["specimens"][K]) {
|
|
28
|
+
const id = generateId()
|
|
29
|
+
this.clade.setSpecimen(id, kind, specimen)
|
|
30
|
+
const item = new CodexItem(this, id)
|
|
31
|
+
this.#items.set(id, item)
|
|
32
|
+
return item.signal.value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** add an item to the hierachy as a root root */
|
|
36
|
+
root<I extends CodexItem<Sc>>(item: I) {
|
|
37
|
+
this.hierarchy.establishRoot(item.id)
|
|
38
|
+
return item.signal.value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** get an item by its id */
|
|
42
|
+
require(id: Id): CodexItem<Sc> {
|
|
43
|
+
return this.#items.require(id).signal.value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
import "@benev/slate/x/node.js"
|
|
3
|
+
import {repeat_string} from "@benev/turtle"
|
|
4
|
+
|
|
5
|
+
import {Codex} from "./codex.js"
|
|
6
|
+
import {AsSchema, Taxon} from "./parts/types.js"
|
|
7
|
+
|
|
8
|
+
//
|
|
9
|
+
// a Schema is all the type information for customizing Quay
|
|
10
|
+
// - *taxon* is like "folders have this icon"
|
|
11
|
+
// - *specimen* is like "this particular folder has this label"
|
|
12
|
+
//
|
|
13
|
+
export type FilesystemSchema = AsSchema<{
|
|
14
|
+
taxon: Taxon
|
|
15
|
+
specimens: {
|
|
16
|
+
folder: {label: string}
|
|
17
|
+
file: {label: string}
|
|
18
|
+
}
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
// we create a codex that uses our schema, and specifies these icons
|
|
22
|
+
const codex = Codex.setup<FilesystemSchema>({
|
|
23
|
+
folder: {icon: "📁"},
|
|
24
|
+
file: {icon: "📄"},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// let's create a folder with some crap
|
|
28
|
+
const folder = codex.create("folder", {label: "project"})
|
|
29
|
+
const readme = codex.create("file", {label: "README.md"})
|
|
30
|
+
const changelog = codex.create("file", {label: "CHANGELOG.md"})
|
|
31
|
+
|
|
32
|
+
// let's create a subfolder with more crap
|
|
33
|
+
const src = codex.create("folder", {label: "src"})
|
|
34
|
+
const index = codex.create("file", {label: "index.ts"})
|
|
35
|
+
const types = codex.create("file", {label: "types.ts"})
|
|
36
|
+
|
|
37
|
+
// we attach the folder as the root, and attach its children
|
|
38
|
+
codex.root(folder)
|
|
39
|
+
.attach(readme)
|
|
40
|
+
.attach(changelog)
|
|
41
|
+
.attach(src) // <-- this is the subfolder
|
|
42
|
+
|
|
43
|
+
// and we attach the subfolder items to it
|
|
44
|
+
src
|
|
45
|
+
.attach(index)
|
|
46
|
+
.attach(types)
|
|
47
|
+
|
|
48
|
+
// we can crawl any folder!
|
|
49
|
+
// 👇
|
|
50
|
+
for (const [item, ancestors] of folder.crawl()) {
|
|
51
|
+
const indent = repeat_string(ancestors.length, " ")
|
|
52
|
+
const specimen = item.specimen
|
|
53
|
+
const label = item.kind === "folder"
|
|
54
|
+
? specimen.label + "/"
|
|
55
|
+
: specimen.label
|
|
56
|
+
console.log(indent + label)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// project/
|
|
60
|
+
// README.md
|
|
61
|
+
// CHANGELOG.md
|
|
62
|
+
// src/
|
|
63
|
+
// index.ts
|
|
64
|
+
// types.ts
|
|
65
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG, sub} from "@e280/stz"
|
|
3
|
+
import {Taxonomy} from "./taxonomy.js"
|
|
4
|
+
import {AnySpecimen, Id, Kind, Schema} from "./types.js"
|
|
5
|
+
|
|
6
|
+
/** Tracks what kind and specimen data is associated with each item id. */
|
|
7
|
+
export class Clade<Sc extends Schema> {
|
|
8
|
+
onChange = sub()
|
|
9
|
+
#specimens = new MapG<Id, [Kind<Sc>, AnySpecimen<Sc>]>()
|
|
10
|
+
|
|
11
|
+
constructor(public taxonomy: Taxonomy<Sc>) {}
|
|
12
|
+
|
|
13
|
+
getSpecimen<K extends Kind<Sc> = Kind<Sc>>(id: string) {
|
|
14
|
+
return this.#specimens.require(id) as [K, Sc["specimens"][K]]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setSpecimen<K extends Kind<Sc>>(id: string, kind: K, specimen: Sc["specimens"][K]) {
|
|
18
|
+
this.#specimens.set(id, [kind, specimen])
|
|
19
|
+
this.onChange.pub()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
query<K extends Kind<Sc> = Kind<Sc>>(id: Id) {
|
|
23
|
+
const [kind, specimen] = this.getSpecimen<K>(id)
|
|
24
|
+
const taxon = this.taxonomy.taxon(kind)
|
|
25
|
+
return {kind, taxon, specimen}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
|
|
2
|
+
import {signal} from "@benev/slate"
|
|
3
|
+
import {Codex} from "../codex.js"
|
|
4
|
+
import {Id, Kind, Schema} from "./types.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Codex Item
|
|
8
|
+
* - Quay's concept of a nestable thing.
|
|
9
|
+
* - ergonomic handle which consolidates taxonomy, clade, and hierarchy functionality.
|
|
10
|
+
*/
|
|
11
|
+
export class CodexItem<Sc extends Schema = any, K extends Kind<Sc> = Kind<Sc>> {
|
|
12
|
+
signal = signal(this)
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private codex: Codex<Sc>,
|
|
16
|
+
public id: Id,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
get kind(): K {
|
|
20
|
+
return this.codex.clade.query<K>(this.id).kind
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isKind<K2 extends Kind<Sc>>(kind: K2): this is CodexItem<Sc, K2> {
|
|
24
|
+
return (kind as any === this.kind)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** get information about the type of entity */
|
|
28
|
+
get taxon(): Sc["taxon"] {
|
|
29
|
+
return this.codex.clade.query(this.id).taxon
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** get information about this specific entity */
|
|
33
|
+
get specimen(): Sc["specimens"][K] {
|
|
34
|
+
return this.codex.clade.query<K>(this.id).specimen
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** get this item's parent item, or undefined if it's not attached */
|
|
38
|
+
get parent(): CodexItem<Sc> | undefined {
|
|
39
|
+
const parent = this.codex.hierarchy.getParent(this.id)
|
|
40
|
+
return parent
|
|
41
|
+
? this.codex.require(parent) as CodexItem<Sc>
|
|
42
|
+
: undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** get an array of this item's children */
|
|
46
|
+
get children(): CodexItem<Sc>[] {
|
|
47
|
+
// TODO actually replace this with a children() iterator, could be a perf win for supermassive sets
|
|
48
|
+
return [...this.codex.hierarchy.getChildren(this.id)]
|
|
49
|
+
.map(id => this.codex.require(id))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** add items as children */
|
|
53
|
+
attach(...items: CodexItem<Sc>[]) {
|
|
54
|
+
this.codex.hierarchy.attach(this.id, ...items.map(i => i.id))
|
|
55
|
+
this.signal.publish()
|
|
56
|
+
return this
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** detach this item from the hierarchy completely */
|
|
60
|
+
detach() {
|
|
61
|
+
this.codex.hierarchy.detach(this.id)
|
|
62
|
+
this.signal.publish()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** create a new item that will be a child */
|
|
66
|
+
create<K extends keyof Sc["specimens"]>(kind: K, specimen: Sc["specimens"][K]) {
|
|
67
|
+
const item = this.codex.create(kind, specimen)
|
|
68
|
+
this.attach(item)
|
|
69
|
+
return item
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** create a new item that will be a child */
|
|
73
|
+
destroy() {
|
|
74
|
+
this.codex.hierarchy.destroy(this.id)
|
|
75
|
+
this.signal.publish()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** iterate over this item and all its descendants */
|
|
79
|
+
*crawl(predicate: (item: CodexItem<Sc>, path: CodexItem<Sc>[]) => boolean = () => true) {
|
|
80
|
+
const iterator = this.codex.hierarchy.crawl(
|
|
81
|
+
this.id,
|
|
82
|
+
(id, path) => predicate(
|
|
83
|
+
this.codex.require(id),
|
|
84
|
+
path.map(id => this.codex.require(id)),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
for (const [id, path] of iterator)
|
|
88
|
+
yield [
|
|
89
|
+
this.codex.require(id),
|
|
90
|
+
path.map(id => this.codex.require(id)),
|
|
91
|
+
] as [CodexItem<Sc>, CodexItem<Sc>[]]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Id} from "./types.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tree of nestable things.
|
|
7
|
+
* - Things can have multiple children.
|
|
8
|
+
* - Things can only have one parent.
|
|
9
|
+
* - Things can be orphans (bring your own concept of a "root").
|
|
10
|
+
*/
|
|
11
|
+
export class Hierarchy {
|
|
12
|
+
#children = new MapG<Id, Set<Id>>()
|
|
13
|
+
#parents = new MapG<Id, Id>()
|
|
14
|
+
|
|
15
|
+
has(id: Id) {
|
|
16
|
+
return this.#children.has(id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getChildren(id: Id) {
|
|
20
|
+
return this.#children.require(id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getParent(id: Id) {
|
|
24
|
+
return this.#parents.get(id)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** establish an id as a root with no parents, but ready to accept children */
|
|
28
|
+
establishRoot(root: Id) {
|
|
29
|
+
this.#children.set(root, new Set())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** give a parent some children */
|
|
33
|
+
attach(parent: Id, ...children: Id[]) {
|
|
34
|
+
const siblings = this.getChildren(parent)
|
|
35
|
+
for (const childId of children) {
|
|
36
|
+
if (this.getParent(childId))
|
|
37
|
+
throw new Error(`child already has parent`)
|
|
38
|
+
siblings.add(childId)
|
|
39
|
+
this.#parents.set(childId, parent)
|
|
40
|
+
this.#children.set(childId, new Set())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** detach this id from its parent in the hierarchy */
|
|
45
|
+
detach(id: Id) {
|
|
46
|
+
if (!this.has(id))
|
|
47
|
+
return undefined
|
|
48
|
+
const parent = this.getParent(id)
|
|
49
|
+
if (parent) {
|
|
50
|
+
const siblings = this.getChildren(parent)
|
|
51
|
+
siblings.delete(id)
|
|
52
|
+
this.#parents.delete(id)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** destroy all relations associated with this id, and all its descendants */
|
|
57
|
+
destroy(id: Id) {
|
|
58
|
+
const tree = [...this.crawl(id)]
|
|
59
|
+
for (const [id] of tree) {
|
|
60
|
+
this.#children.delete(id)
|
|
61
|
+
this.#parents.delete(id)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** iterate over this id and all its descendants */
|
|
66
|
+
*crawl(id: Id, predicate: (id: Id, path: Id[]) => boolean = () => true) {
|
|
67
|
+
const todo: [Id, Id[]][] = [[id, []]]
|
|
68
|
+
const seen = new Set<Id>()
|
|
69
|
+
|
|
70
|
+
while (todo.length) {
|
|
71
|
+
const [id, path] = todo.shift()!
|
|
72
|
+
if (seen.has(id) || !predicate(id, path)) continue
|
|
73
|
+
seen.add(id)
|
|
74
|
+
|
|
75
|
+
yield [id, path] as [Id, Id[]]
|
|
76
|
+
|
|
77
|
+
for (const childId of this.getChildren(id))
|
|
78
|
+
todo.push([childId, [...path, id]])
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Kind, Schema, Taxons} from "./types.js"
|
|
4
|
+
|
|
5
|
+
/** Defines the static details about each "kind" of data */
|
|
6
|
+
export class Taxonomy<Sc extends Schema> {
|
|
7
|
+
#taxons = new MapG<Kind<Sc>, Sc["taxon"]>()
|
|
8
|
+
|
|
9
|
+
constructor(taxons: Taxons<Sc>) {
|
|
10
|
+
for (const [kind, taxon] of Object.entries(taxons))
|
|
11
|
+
this.#taxons.set(kind, taxon)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
taxon<K extends Kind<Sc>>(kind: K) {
|
|
15
|
+
return this.#taxons.require(kind) as Sc["taxon"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import {Content} from "@benev/slate"
|
|
3
|
+
|
|
4
|
+
export type Id = string
|
|
5
|
+
|
|
6
|
+
export type Taxon = {
|
|
7
|
+
icon: Content
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Specimen = {label: string}
|
|
11
|
+
|
|
12
|
+
export type Specimens = {[key: string]: Specimen}
|
|
13
|
+
export type AsSpecimens<S extends Specimens> = S
|
|
14
|
+
|
|
15
|
+
export type Schema = {
|
|
16
|
+
taxon: Taxon
|
|
17
|
+
specimens: Specimens
|
|
18
|
+
}
|
|
19
|
+
export type AsSchema<Sc extends Schema> = Sc
|
|
20
|
+
export type Kind<Sc extends Schema> = keyof Sc["specimens"]
|
|
21
|
+
export type Taxons<Sc extends Schema> = {[K in Kind<Sc>]: Sc["taxon"]}
|
|
22
|
+
export type AnySpecimen<Sc extends Schema> = Sc["specimens"][Kind<Sc>]
|
|
23
|
+
|