@e280/quay 0.0.0-12 → 0.0.0-13

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 (46) hide show
  1. package/README.md +59 -0
  2. package/package.json +2 -1
  3. package/s/cellar/cask.ts +13 -7
  4. package/s/cellar/cellar.test.ts +20 -18
  5. package/s/cellar/cellar.ts +3 -3
  6. package/s/cellar/forklift.ts +2 -2
  7. package/s/cellar/memory-forklift.ts +4 -3
  8. package/s/cellar/opfs-forklift.ts +4 -6
  9. package/s/dom/components/browser/style.css.ts +30 -30
  10. package/s/dom/components/dropzone/style.css.ts +11 -12
  11. package/s/dom/components/filter/component.ts +2 -2
  12. package/s/dom/components/searchbar/component.ts +5 -2
  13. package/s/dom/components/sort/component.ts +3 -3
  14. package/s/dom/theme.css.ts +28 -0
  15. package/s/dom/views/tree/style.css.ts +9 -9
  16. package/s/logic/presets/media/library.ts +2 -3
  17. package/x/cellar/cask.d.ts +4 -4
  18. package/x/cellar/cask.js +13 -9
  19. package/x/cellar/cask.js.map +1 -1
  20. package/x/cellar/cellar.d.ts +1 -1
  21. package/x/cellar/cellar.js +3 -3
  22. package/x/cellar/cellar.js.map +1 -1
  23. package/x/cellar/cellar.test.js +19 -18
  24. package/x/cellar/cellar.test.js.map +1 -1
  25. package/x/cellar/forklift.d.ts +2 -2
  26. package/x/cellar/memory-forklift.d.ts +2 -2
  27. package/x/cellar/memory-forklift.js +3 -2
  28. package/x/cellar/memory-forklift.js.map +1 -1
  29. package/x/cellar/opfs-forklift.d.ts +2 -2
  30. package/x/cellar/opfs-forklift.js +3 -5
  31. package/x/cellar/opfs-forklift.js.map +1 -1
  32. package/x/demo/main.bundle.min.js +104 -71
  33. package/x/demo/main.bundle.min.js.map +2 -2
  34. package/x/dom/components/browser/style.css.js +30 -29
  35. package/x/dom/components/browser/style.css.js.map +1 -1
  36. package/x/dom/components/dropzone/style.css.js +11 -11
  37. package/x/dom/components/filter/component.js +2 -2
  38. package/x/dom/components/searchbar/component.js +5 -1
  39. package/x/dom/components/searchbar/component.js.map +1 -1
  40. package/x/dom/components/sort/component.js +3 -3
  41. package/x/dom/theme.css.js +28 -0
  42. package/x/dom/theme.css.js.map +1 -1
  43. package/x/dom/views/tree/style.css.js +9 -9
  44. package/x/index.html +2 -2
  45. package/x/logic/presets/media/library.js +2 -3
  46. package/x/logic/presets/media/library.js.map +1 -1
package/README.md CHANGED
@@ -143,3 +143,62 @@ The scope passed to `open()` separates media libraries.
143
143
  const projectA = await MediaLibrary.open("project-a")
144
144
  const projectB = await MediaLibrary.open("project-b")
145
145
  ```
146
+
147
+ <br/>
148
+
149
+ ## Styling
150
+
151
+ Customize quay through its css vars
152
+
153
+ ```css
154
+ :root {
155
+ --quay-surface: #111;
156
+ --quay-border: #222;
157
+ --quay-text: #333;
158
+ --quay-muted: #777;
159
+ --quay-accent: #0ea5e9;
160
+ --quay-radius: 6px;
161
+ }
162
+ ```
163
+
164
+ You can scope those variables to one area or one component.
165
+
166
+ ```css
167
+ .media-panel {
168
+ --quay-surface: #18181b;
169
+ --quay-border: #27272a;
170
+ }
171
+
172
+ quay-browser {
173
+ --quay-browser-thumb-width: 120px;
174
+ --quay-browser-thumb-height: 72px;
175
+ }
176
+ ```
177
+
178
+ Public tokens:
179
+
180
+ ```css
181
+ --quay-surface
182
+ --quay-surface-hover
183
+ --quay-surface-selected
184
+ --quay-border
185
+ --quay-text
186
+ --quay-muted
187
+ --quay-accent
188
+ --quay-radius
189
+ --quay-browser-thumb-width
190
+ --quay-browser-thumb-height
191
+ ```
192
+
193
+ Quay is built on Shoelace, and its internal theme uses Shoelace tokens as fallbacks.
194
+ For deeper control, set Shoelace tokens in your app or scoped around Quay,
195
+ watch out for conflicts if you already use shoelace components in your app.
196
+
197
+ ```css
198
+ .media-panel {
199
+ --sl-color-primary-600: #575757;
200
+ --sl-input-focus-ring-color: rgb(119 119 119 / 35%);
201
+ --sl-border-radius-medium: 3px;
202
+ --sl-spacing-x-small: 0.5em;
203
+ }
204
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/quay",
3
- "version": "0.0.0-12",
3
+ "version": "0.0.0-13",
4
4
  "description": "File-browser and outliner UI for the web",
5
5
  "author": "Przemysław Gałęzki",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  "@e280/sly": "^0.4.0-next.5",
17
17
  "@e280/strata": "^0.4.0-next.8",
18
18
  "@e280/stz": "^0.0.0-22",
19
+ "@noble/hashes": "^2.2.0",
19
20
  "lit": "^3.3.1"
20
21
  },
21
22
  "devDependencies": {
package/s/cellar/cask.ts CHANGED
@@ -1,18 +1,24 @@
1
1
 
2
2
  import {Hex} from "@e280/stz"
3
+ import {blake3} from "@noble/hashes/blake3.js"
3
4
 
4
5
  /** File with a hash label */
5
6
  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))
7
+ static async hash(file: Blob) {
8
+ const hasher = blake3.create()
9
+
10
+ for await (const chunk of file.stream())
11
+ hasher.update(chunk)
12
+
13
+ const digest = hasher.digest()
14
+ return Hex.fromBytes(digest)
9
15
  }
10
16
 
11
- static async make(bytes: Uint8Array) {
12
- const hash = await this.hash(bytes)
13
- return new this(hash, bytes)
17
+ static async make(file: Blob) {
18
+ const hash = await this.hash(file)
19
+ return new this(hash, file)
14
20
  }
15
21
 
16
- constructor(public hash: string, public bytes: Uint8Array) {}
22
+ constructor(public hash: string, public file: Blob) {}
17
23
  }
18
24
 
@@ -6,36 +6,38 @@ import {Cask} from "./cask.js"
6
6
  import {Cellar} from "./cellar.js"
7
7
  import {MemoryForklift} from "./memory-forklift.js"
8
8
 
9
+ const blob = (text: string) => new Blob([Txt.toBytes(text)])
10
+
9
11
  export default Science.suite({
10
12
  "save + load roundtrip": test(async() => {
11
13
  const cellar = new Cellar()
12
14
  const text = "hello world"
13
- const bytes = Txt.toBytes(text)
14
- await cellar.save(bytes)
15
+ const file = blob(text)
16
+ await cellar.save(file)
15
17
 
16
- const hash = await Cask.hash(bytes)
18
+ const hash = await Cask.hash(file)
17
19
  expect(await cellar.has(hash)).is(true)
18
20
 
19
21
  const loaded = await cellar.load(hash)
20
22
  expect(loaded.hash).is(hash)
21
- expect(Txt.fromBytes(loaded.bytes)).is(text)
23
+ expect(await loaded.file.text()).is(text)
22
24
  }),
23
25
 
24
26
  "save is idempotent": test(async() => {
25
27
  const cellar = new Cellar()
26
- const bytes = new TextEncoder().encode("foo bar")
27
- await cellar.save(bytes)
28
- await cellar.save(bytes) // again
28
+ const file = blob("foo bar")
29
+ await cellar.save(file)
30
+ await cellar.save(file) // again
29
31
 
30
- const hash = await Cask.hash(bytes)
32
+ const hash = await Cask.hash(file)
31
33
  expect(await cellar.has(hash)).is(true)
32
34
  }),
33
35
 
34
36
  "throws on corruption": test(async() => {
35
37
  const forklift = new MemoryForklift()
36
38
  const cellar = new Cellar(forklift)
37
- const good = new TextEncoder().encode("valid data")
38
- const bad = new TextEncoder().encode("corrupt data")
39
+ const good = blob("valid data")
40
+ const bad = blob("corrupt data")
39
41
 
40
42
  const hash = await Cask.hash(good)
41
43
  await forklift.save(hash, bad)
@@ -45,10 +47,10 @@ export default Science.suite({
45
47
 
46
48
  "delete removes file": test(async() => {
47
49
  const cellar = new Cellar()
48
- const bytes = new TextEncoder().encode("temporary data")
49
- await cellar.save(bytes)
50
+ const file = blob("temporary data")
51
+ await cellar.save(file)
50
52
 
51
- const hash = await Cask.hash(bytes)
53
+ const hash = await Cask.hash(file)
52
54
  expect(await cellar.has(hash)).is(true)
53
55
 
54
56
  await cellar.delete(hash)
@@ -67,7 +69,7 @@ export default Science.suite({
67
69
  const texts = ["a", "b", "c"]
68
70
 
69
71
  for (const t of texts)
70
- await cellar.save(Txt.toBytes(t))
72
+ await cellar.save(blob(t))
71
73
 
72
74
  const hashes = []
73
75
  for await (const hash of cellar.list())
@@ -75,16 +77,16 @@ export default Science.suite({
75
77
 
76
78
  expect(hashes.length).is(3)
77
79
  for (const t of texts) {
78
- const h = await Cask.hash(Txt.toBytes(t))
80
+ const h = await Cask.hash(blob(t))
79
81
  expect(hashes.includes(h)).is(true)
80
82
  }
81
83
  }),
82
84
 
83
85
  "clear removes all entries": test(async() => {
84
86
  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"))
87
+ await cellar.save(blob("one"))
88
+ await cellar.save(blob("two"))
89
+ await cellar.save(blob("three"))
88
90
 
89
91
  let count = 0
90
92
  for await (const _ of cellar.list()) count++
@@ -28,10 +28,10 @@ export class Cellar {
28
28
  return this.forklift.has(hash)
29
29
  }
30
30
 
31
- async save(bytes: Uint8Array) {
32
- const cask = await Cask.make(bytes)
31
+ async save(file: Blob) {
32
+ const cask = await Cask.make(file)
33
33
  if (!await this.forklift.has(cask.hash))
34
- await this.forklift.save(cask.hash, cask.bytes)
34
+ await this.forklift.save(cask.hash, cask.file)
35
35
  return cask
36
36
  }
37
37
 
@@ -2,8 +2,8 @@
2
2
  export type Forklift = {
3
3
  list(): AsyncIterable<string>
4
4
  has(label: string): Promise<boolean>
5
- save(label: string, bytes: Uint8Array): Promise<void>
6
- load(label: string): Promise<Uint8Array>
5
+ save(label: string, file: Blob): Promise<void>
6
+ load(label: string): Promise<Blob>
7
7
  delete(label: string): Promise<void>
8
8
  }
9
9
 
@@ -14,12 +14,13 @@ export class MemoryForklift implements Forklift {
14
14
  return this.#map.has(label)
15
15
  }
16
16
 
17
- async save(label: string, bytes: Uint8Array): Promise<void> {
17
+ async save(label: string, file: Blob): Promise<void> {
18
+ const bytes = new Uint8Array(await file.arrayBuffer())
18
19
  this.#map.set(label, bytes)
19
20
  }
20
21
 
21
- async load(label: string): Promise<Uint8Array> {
22
- return this.#map.require(label)
22
+ async load(label: string): Promise<Blob> {
23
+ return new Blob([this.#map.require(label)])
23
24
  }
24
25
 
25
26
  async delete(label: string) {
@@ -28,18 +28,16 @@ export class OpfsForklift implements Forklift {
28
28
  }
29
29
  }
30
30
 
31
- async save(label: string, bytes: Uint8Array): Promise<void> {
31
+ async save(label: string, file: Blob): Promise<void> {
32
32
  const fileHandle = await this.directory.getFileHandle(label, {create: true})
33
33
  const writable = await fileHandle.createWritable()
34
- await writable.write(bytes)
34
+ await writable.write(file)
35
35
  await writable.close()
36
36
  }
37
37
 
38
- async load(label: string): Promise<Uint8Array> {
38
+ async load(label: string): Promise<Blob> {
39
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)
40
+ return await fileHandle.getFile()
43
41
  }
44
42
 
45
43
  async delete(label: string) {
@@ -5,38 +5,39 @@ export default css`
5
5
  .toolbar {
6
6
  display: flex;
7
7
  justify-content: flex-end;
8
- padding: 0.5rem;
8
+ padding: var(--space-sm);
9
9
 
10
10
  sl-button {
11
- background: #0F0F11;
12
- border-radius: 5px;
11
+ background: var(--surface);
12
+ border-radius: var(--radius-small);
13
13
 
14
14
  &[data-active=true] {
15
- background: var(--sl-color-neutral-0);
15
+ background: var(--surface-active);
16
16
  }
17
17
 
18
18
  &::part(base) {
19
19
  display: flex;
20
20
  align-items: center;
21
- height: 30px;
21
+ height: var(--control-height);
22
+ border-radius: var(--radius-small);
22
23
  }
23
24
 
24
25
  &::part(label) {
25
26
  display: flex;
26
- width: 40px;
27
+ width: var(--control-width);
27
28
  }
28
29
  }
29
30
  }
30
31
 
31
32
  .tiles,
32
33
  .details {
33
- padding: 0.5rem;
34
+ padding: var(--space-sm);
34
35
  }
35
36
 
36
37
  .tiles {
37
38
  display: flex;
38
39
  flex-wrap: wrap;
39
- row-gap: 1em;
40
+ row-gap: var(--space-md);
40
41
  width: 100%;
41
42
 
42
43
  .item {
@@ -50,11 +51,11 @@ export default css`
50
51
  display: flex;
51
52
  align-items: center;
52
53
  justify-content: center;
53
- width: 100px;
54
- height: 60px;
55
- border: 2px solid #0D0D0E;
56
- border-radius: 5px;
57
- background: #0F0F11;
54
+ width: var(--quay-browser-thumb-width, 100px);
55
+ height: var(--quay-browser-thumb-height, 60px);
56
+ border: var(--border-width) solid var(--border);
57
+ border-radius: var(--radius-small);
58
+ background: var(--surface);
58
59
  pointer-events: none;
59
60
 
60
61
  img {
@@ -64,14 +65,14 @@ export default css`
64
65
  }
65
66
 
66
67
  .label {
67
- margin-top: 0.5em;
68
+ margin-top: var(--space-xs);
68
69
  max-width: 150px;
69
- font-size: 0.85rem;
70
+ font-size: var(--font-size-sm);
70
71
  text-align: center;
71
72
  white-space: nowrap;
72
73
  overflow: hidden;
73
74
  text-overflow: ellipsis;
74
- color: var(--sl-color-neutral-700);
75
+ color: var(--text-muted);
75
76
  }
76
77
  }
77
78
  }
@@ -79,40 +80,39 @@ export default css`
79
80
  .details {
80
81
  .card {
81
82
  display: flex;
82
- gap: 1rem;
83
- padding: 0.8rem;
84
- border: 2px solid #0D0D0E;
85
- background: #0F0F11;
86
- border-radius: 6px;
87
- margin-bottom: 0.5rem;
83
+ gap: var(--space-md);
84
+ padding: var(--space-sm);
85
+ border: var(--border-width) solid var(--border);
86
+ background: var(--surface);
87
+ border-radius: var(--radius);
88
+ margin-bottom: var(--space-sm);
88
89
  align-items: center;
89
90
 
90
91
  .meta {
91
92
  .name {
92
93
  font-weight: 500;
93
- color: var(--sl-color-neutral-800);
94
+ color: var(--text);
94
95
  }
95
96
  .type {
96
- font-size: 0.75rem;
97
- color: var(--sl-color-neutral-500);
97
+ font-size: var(--font-size-xs);
98
+ color: var(--text-subtle);
98
99
  }
99
100
  }
100
101
  }
101
102
  }
102
103
 
103
104
  sl-icon {
104
- font-size: 2rem;
105
- color: var(--sl-color-neutral-300);
105
+ font-size: var(--icon-size);
106
+ color: var(--icon);
106
107
  }
107
108
 
108
109
  sl-tooltip {
109
110
  --sl-tooltip-arrow-size: 0;
110
111
 
111
112
  &::part(body) {
112
- background: #0D0D0E;
113
- color: var(--sl-color-neutral-700);
113
+ background: var(--border);
114
+ color: var(--text-muted);
114
115
  }
115
116
  }
116
117
 
117
118
  `
118
-
@@ -9,16 +9,16 @@ export default css`
9
9
  align-items: center;
10
10
  justify-content: center;
11
11
  position: relative;
12
- border: 2px dashed var(--sl-color-neutral-300);
13
- border-radius: 6px;
14
- transition: background 0.2s;
12
+ border: var(--border-width) dashed var(--border-muted);
13
+ border-radius: var(--radius);
14
+ transition: background var(--transition), border-color var(--transition), color var(--transition);
15
15
  }
16
16
 
17
17
  .dz-input {
18
18
  width: 100%;
19
19
  height: 100%;
20
- z-index: 999;
21
- font-size: 14px;
20
+ z-index: var(--z-overlay);
21
+ font-size: var(--font-size-sm);
22
22
  cursor: pointer;
23
23
  opacity: 0;
24
24
 
@@ -28,15 +28,15 @@ export default css`
28
28
  }
29
29
 
30
30
  .dropzone:hover {
31
- background: var(--sl-color-gray-50);
31
+ background: var(--surface-hover);
32
32
  }
33
33
 
34
34
  .dropzone[data-hovering] {
35
- background: var(--sl-color-gray-50);
36
- border-color: var(--sl-color-primary-500);
35
+ background: var(--surface-hover);
36
+ border-color: var(--primary);
37
37
 
38
38
  & .dz-info {
39
- color: var(--sl-color-primary-300);
39
+ color: var(--primary-soft);
40
40
  }
41
41
  }
42
42
 
@@ -46,9 +46,8 @@ export default css`
46
46
  flex-direction: column;
47
47
  align-items: center;
48
48
  justify-content: center;
49
- gap: 0.5em;
50
- color: var(--sl-color-neutral-700);
49
+ gap: var(--space-sm);
50
+ color: var(--text-muted);
51
51
  }
52
52
 
53
53
  `
54
-
@@ -12,12 +12,12 @@ export const QuayFilter = shadowElement(() => {
12
12
 
13
13
  useStyles(themeCss, css`
14
14
  sl-menu {
15
- margin-top: 0.3em;
15
+ margin-top: var(--space-xs);
16
16
 
17
17
  .item {
18
18
  display: flex;
19
19
  align-items: center;
20
- gap: 0.5em;
20
+ gap: var(--space-sm);
21
21
  }
22
22
  }
23
23
  `)
@@ -7,8 +7,12 @@ import themeCss from '../../theme.css.js'
7
7
  import {findLocalGroup} from '../../utils/find-local-group.js'
8
8
 
9
9
  const styleCss = css`
10
+ sl-input::part(base) {
11
+ border-radius: var(--radius);
12
+ }
13
+
10
14
  sl-input::part(prefix) {
11
- padding-left: 0.5em;
15
+ padding-left: var(--space-sm);
12
16
  }
13
17
  `
14
18
 
@@ -29,4 +33,3 @@ export const QuaySearchbar = shadowElement(() => {
29
33
  </sl-input>
30
34
  `
31
35
  })
32
-
@@ -12,16 +12,16 @@ export const QuaySort = shadowElement(() => {
12
12
 
13
13
  useStyles(themeCss, css`
14
14
  sl-button sl-icon {
15
- font-size: 14px;
15
+ font-size: var(--font-size-sm);
16
16
  }
17
17
 
18
18
  sl-menu {
19
- margin-top: 0.3em;
19
+ margin-top: var(--space-xs);
20
20
 
21
21
  .item {
22
22
  display: flex;
23
23
  align-items: center;
24
- gap: 0.5em;
24
+ gap: var(--space-sm);
25
25
  }
26
26
  }
27
27
  `)
@@ -2,6 +2,34 @@
2
2
  import {css} from "lit"
3
3
  export default css`@layer theme, view; @layer theme {
4
4
 
5
+ :host {
6
+ --surface: var(--quay-surface, #0f0f11);
7
+ --surface-active: var(--quay-surface-hover, var(--sl-color-neutral-0, #fff));
8
+ --surface-hover: var(--quay-surface-hover, var(--sl-color-gray-50, #f8f9fa));
9
+ --surface-selected: var(--quay-surface-selected, rgb(0 0 0 / 40%));
10
+ --border: var(--quay-border, #0d0d0e);
11
+ --border-muted: var(--quay-border, var(--sl-color-neutral-300, #d0d0d0));
12
+ --border-width: 1px;
13
+ --text: var(--quay-text, var(--sl-color-neutral-800, #333));
14
+ --text-muted: var(--quay-muted, var(--sl-color-neutral-700, #555));
15
+ --text-subtle: var(--quay-muted, var(--sl-color-neutral-500, #777));
16
+ --icon: var(--quay-muted, var(--sl-color-neutral-300, #d0d0d0));
17
+ --primary: var(--quay-accent, var(--sl-color-primary-500, #0ea5e9));
18
+ --primary-soft: color-mix(in srgb, var(--primary), white 65%);
19
+ --radius: var(--quay-radius, var(--sl-border-radius-medium, 6px));
20
+ --radius-small: var(--sl-border-radius-small, var(--radius));
21
+ --space-xs: var(--sl-spacing-2x-small, 0.3em);
22
+ --space-sm: var(--sl-spacing-x-small, 0.5rem);
23
+ --space-md: var(--sl-spacing-medium, 1rem);
24
+ --font-size-xs: var(--sl-font-size-x-small, 0.75rem);
25
+ --font-size-sm: var(--sl-font-size-small, 0.85rem);
26
+ --control-height: 30px;
27
+ --control-width: 40px;
28
+ --icon-size: 2rem;
29
+ --z-overlay: 999;
30
+ --transition: 0.2s ease;
31
+ }
32
+
5
33
  * {
6
34
  margin: 0;
7
35
  padding: 0;
@@ -7,12 +7,12 @@ sl-tree-item::part(expand-button) {
7
7
  }
8
8
 
9
9
  sl-tree-item::part(base) {
10
- min-height: 30px;
10
+ min-height: var(--control-height);
11
11
  justify-content: center;
12
12
  }
13
13
 
14
14
  sl-icon:is(.item) {
15
- padding: 0.5em;
15
+ padding: var(--space-sm);
16
16
  }
17
17
 
18
18
  sl-tree-item:not([data-type=folder])::part(expand-button) {
@@ -35,11 +35,11 @@ sl-tree-item:not([data-type=folder])::part(expand-button) {
35
35
  }
36
36
 
37
37
  sl-tree-item {
38
- min-height: 30px;
39
- transition: background 0.2s ease, border-radius 0.2s ease;
38
+ min-height: var(--control-height);
39
+ transition: background var(--transition), border-radius var(--transition);
40
40
 
41
41
  ::part(label) {
42
- gap: 0.5em;
42
+ gap: var(--space-sm);
43
43
  }
44
44
 
45
45
  .folder {
@@ -49,15 +49,15 @@ sl-tree-item {
49
49
  }
50
50
 
51
51
  ::part(item--selected) {
52
- background: rgba(0, 0, 0, 0.4);
53
- min-height: 30px;
52
+ background: var(--surface-selected);
53
+ min-height: var(--control-height);
54
54
  }
55
55
 
56
56
  }
57
57
 
58
58
  sl-tree-item[data-hover] {
59
- border-radius: 5px;
60
- background: rgba(0, 0, 0, 0.4);
59
+ border-radius: var(--radius-small);
60
+ background: var(--surface-selected);
61
61
  }
62
62
 
63
63
  `
@@ -60,8 +60,7 @@ export class MediaLibrary extends MediaGroup {
60
60
  }
61
61
 
62
62
  async #storeFile(file: File, parent: CodexItem<MediaSchema>) {
63
- const bytes = new Uint8Array(await file.arrayBuffer())
64
- const cask = await this.cellar.save(bytes)
63
+ const cask = await this.cellar.save(file)
65
64
  const existing = await this.#index.get(cask.hash)
66
65
  const now = Date.now()
67
66
  const record: MediaRecord = {
@@ -126,7 +125,7 @@ export class MediaLibrary extends MediaGroup {
126
125
 
127
126
  async #loadPreview(record: MediaRecord) {
128
127
  const cask = await this.cellar.load(record.hash)
129
- const blob = new Blob([cask.bytes], {type: record.mime || "image/*"})
128
+ const blob = new Blob([cask.file], {type: record.mime || "image/*"})
130
129
  return this.#setPreview(record.hash, URL.createObjectURL(blob))
131
130
  }
132
131
 
@@ -1,8 +1,8 @@
1
1
  /** File with a hash label */
2
2
  export declare class Cask {
3
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);
4
+ file: Blob;
5
+ static hash(file: Blob): Promise<string>;
6
+ static make(file: Blob): Promise<Cask>;
7
+ constructor(hash: string, file: Blob);
8
8
  }
package/x/cellar/cask.js CHANGED
@@ -1,19 +1,23 @@
1
1
  import { Hex } from "@e280/stz";
2
+ import { blake3 } from "@noble/hashes/blake3.js";
2
3
  /** File with a hash label */
3
4
  export class Cask {
4
5
  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));
6
+ file;
7
+ static async hash(file) {
8
+ const hasher = blake3.create();
9
+ for await (const chunk of file.stream())
10
+ hasher.update(chunk);
11
+ const digest = hasher.digest();
12
+ return Hex.fromBytes(digest);
9
13
  }
10
- static async make(bytes) {
11
- const hash = await this.hash(bytes);
12
- return new this(hash, bytes);
14
+ static async make(file) {
15
+ const hash = await this.hash(file);
16
+ return new this(hash, file);
13
17
  }
14
- constructor(hash, bytes) {
18
+ constructor(hash, file) {
15
19
  this.hash = hash;
16
- this.bytes = bytes;
20
+ this.file = file;
17
21
  }
18
22
  }
19
23
  //# sourceMappingURL=cask.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"cask.js","sourceRoot":"","sources":["../../s/cellar/cask.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAC7B,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAA;AAE9C,6BAA6B;AAC7B,MAAM,OAAO,IAAI;IAgBG;IAAqB;IAfxC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAU;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;QAE9B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE;YACtC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAErB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;QAC9B,OAAO,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAU;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC5B,CAAC;IAED,YAAmB,IAAY,EAAS,IAAU;QAA/B,SAAI,GAAJ,IAAI,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAM;IAAG,CAAC;CACtD"}