@axium/storage 0.6.0 → 0.6.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/lib/List.svelte CHANGED
@@ -6,7 +6,11 @@
6
6
  import type { StorageItemMetadata } from '@axium/storage/common';
7
7
  import '../styles/list.css';
8
8
 
9
- let { items = $bindable([]), appMode }: { appMode?: boolean; items: StorageItemMetadata[] } = $props();
9
+ let {
10
+ items = $bindable([]),
11
+ appMode,
12
+ emptyText = 'Folder is empty.',
13
+ }: { appMode?: boolean; items: StorageItemMetadata[]; emptyText?: string } = $props();
10
14
 
11
15
  let activeIndex = $state<number>(-1);
12
16
  let activeItem = $derived(activeIndex == -1 ? null : items[activeIndex]);
@@ -40,7 +44,7 @@
40
44
  <dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
41
45
  <span class="name">{item.name}</span>
42
46
  <span>{item.modifiedAt.toLocaleString()}</span>
43
- <span>{formatBytes(item.size)}</span>
47
+ <span>{item.type == 'inode/directory' ? '—' : formatBytes(item.size)}</span>
44
48
  {@render action('rename', 'pencil', i)}
45
49
  {@render action('download', 'download', i)}
46
50
  {@render action('trash', 'trash', i)}
@@ -61,7 +65,7 @@
61
65
  {@render _item(item, i)}
62
66
  {/if}
63
67
  {:else}
64
- <p class="list-empty">Folder is empty.</p>
68
+ <p class="list-empty">{emptyText}</p>
65
69
  {/each}
66
70
  </div>
67
71
 
@@ -69,8 +73,9 @@
69
73
  bind:dialog={dialogs.rename}
70
74
  submitText="Rename"
71
75
  submit={async (data: { name: string }) => {
72
- await updateItemMetadata(activeItem!.id, data);
73
- activeItem!.name = data.name;
76
+ if (!activeItem) throw 'No item is selected';
77
+ await updateItemMetadata(activeItem.id, data);
78
+ activeItem.name = data.name;
74
79
  }}
75
80
  >
76
81
  <div>
@@ -83,8 +88,9 @@
83
88
  submitText="Trash"
84
89
  submitDanger
85
90
  submit={async () => {
86
- await updateItemMetadata(activeItem!.id, { trash: true });
87
- if (activeIndex != -1) items.splice(activeIndex, 1);
91
+ if (!activeItem) throw 'No item is selected';
92
+ await updateItemMetadata(activeItem.id, { trash: true });
93
+ items.splice(activeIndex, 1);
88
94
  }}
89
95
  >
90
96
  <p>Are you sure you want to trash {@render _itemName()}?</p>
@@ -107,10 +113,6 @@
107
113
 
108
114
  <style>
109
115
  .list-item {
110
- display: grid;
111
116
  grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
112
- align-items: center;
113
- gap: 0.5em;
114
- padding: 0.5em 0;
115
117
  }
116
118
  </style>
package/lib/Usage.svelte CHANGED
@@ -8,14 +8,16 @@
8
8
  </script>
9
9
 
10
10
  {#await info || getUserStorageInfo(userId) then info}
11
- <a href="/files/usage">
12
- <NumberBar
13
- max={info.limits.user_size * 1_000_000}
14
- value={info.usage.bytes}
15
- text="Using {formatBytes(info.usage.bytes)} of {formatBytes(info.limits.user_size * 1_000_000)}"
16
- --fill="#345"
17
- />
18
- </a>
11
+ <p>
12
+ <a href="/files/usage">
13
+ <NumberBar
14
+ max={info.limits.user_size * 1_000_000}
15
+ value={info.usage.bytes}
16
+ text="Using {formatBytes(info.usage.bytes)} of {formatBytes(info.limits.user_size * 1_000_000)}"
17
+ --fill="#345"
18
+ />
19
+ </a>
20
+ </p>
19
21
  {:catch error}
20
22
  <p>Couldn't load your uploads.</p>
21
23
  <p>{error.message}</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -40,7 +40,7 @@
40
40
  "peerDependencies": {
41
41
  "@axium/client": ">=0.1.0",
42
42
  "@axium/core": ">=0.5.0",
43
- "@axium/server": ">=0.19.2",
43
+ "@axium/server": ">=0.20.2",
44
44
  "@sveltejs/kit": "^2.27.3",
45
45
  "utilium": "^2.3.8"
46
46
  },
@@ -10,7 +10,7 @@
10
10
  <div class="app">
11
11
  <div class="sidebar">
12
12
  {#each data.tabs as { href, name, icon: i, active }}
13
- <a {href} class={['item', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
13
+ <a {href} class={['item', 'icon-text', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
14
14
  {/each}
15
15
 
16
16
  <div class="usage">
@@ -42,9 +42,6 @@
42
42
  .item {
43
43
  padding: 0.3em 0.5em;
44
44
  border-radius: 0.25em 1em 1em 0.25em;
45
- display: inline-flex;
46
- align-items: center;
47
- gap: 1em;
48
45
  }
49
46
 
50
47
  .item:hover {
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { data }: PageProps = $props();
8
8
 
9
+ let items = $state(data.items!);
9
10
  const item = $state(data.item);
10
11
  </script>
11
12
 
@@ -24,7 +25,7 @@
24
25
  <Icon i="trash-can-undo" /> Restore
25
26
  </button>
26
27
  {:else if item.type == 'inode/directory'}
27
- <StorageList appMode bind:items={data.items!} />
28
+ <StorageList appMode bind:items />
28
29
  {:else}
29
30
  <p>No preview available.</p>
30
31
  {/if}
@@ -2,16 +2,26 @@
2
2
  import { formatBytes } from '@axium/core/format';
3
3
  import { forMime as iconForMime } from '@axium/core/icons';
4
4
  import { FormDialog, Icon } from '@axium/server/components';
5
- import { deleteItem } from '@axium/storage/client';
5
+ import { deleteItem, updateItemMetadata } from '@axium/storage/client';
6
6
  import '@axium/storage/styles/list';
7
7
  import type { PageProps } from './$types';
8
8
 
9
9
  const { data }: PageProps = $props();
10
10
  let items = $state(data.items);
11
- let dialog = $state<HTMLDialogElement>();
11
+ let restoreDialog = $state<HTMLDialogElement>()!;
12
+ let deleteDialog = $state<HTMLDialogElement>()!;
12
13
 
13
14
  let activeIndex = $state<number>(-1);
14
- const activeItem = $derived(items[activeIndex]);
15
+ const activeItem = $derived(activeIndex == -1 ? null : items[activeIndex]);
16
+
17
+ function action(index: number, dialog: () => HTMLDialogElement) {
18
+ return (e: Event) => {
19
+ e.stopPropagation();
20
+ e.preventDefault();
21
+ activeIndex = index;
22
+ dialog().showModal();
23
+ };
24
+ }
15
25
  </script>
16
26
 
17
27
  <svelte:head>
@@ -31,16 +41,11 @@
31
41
  <span class="name">{item.name}</span>
32
42
  <span>{item.modifiedAt.toLocaleString()}</span>
33
43
  <span>{formatBytes(item.size)}</span>
34
- <span
35
- class="action"
36
- onclick={(e: Event) => {
37
- e.stopPropagation();
38
- e.preventDefault();
39
- activeIndex = i;
40
- dialog?.showModal();
41
- }}
42
- >
43
- <Icon i="trash" --size="14px" --fill="#c44" />
44
+ <span class="action" onclick={action(i, () => restoreDialog)}>
45
+ <Icon i="rotate-left" --size="14px" />
46
+ </span>
47
+ <span class="action" onclick={action(i, () => deleteDialog)}>
48
+ <Icon i="trash-can-xmark" --size="14px" --fill="#c44" />
44
49
  </span>
45
50
  </div>
46
51
  {:else}
@@ -48,25 +53,40 @@
48
53
  {/each}
49
54
  </div>
50
55
 
56
+ {#snippet _name()}
57
+ {#if activeItem?.name}<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
58
+ {:else}this
59
+ {/if}
60
+ {/snippet}
61
+
62
+ <FormDialog
63
+ bind:dialog={restoreDialog}
64
+ submitText="Restore"
65
+ submit={async () => {
66
+ if (!activeItem) throw 'No item is selected';
67
+ await updateItemMetadata(activeItem.id, { trash: false });
68
+ items.splice(activeIndex, 1);
69
+ }}
70
+ >
71
+ <p>Restore {@render _name()}?</p>
72
+ </FormDialog>
51
73
  <FormDialog
52
- bind:dialog
74
+ bind:dialog={deleteDialog}
53
75
  submitText="Delete"
54
76
  submitDanger
55
77
  submit={async () => {
78
+ if (!activeItem) throw 'No item is selected';
56
79
  await deleteItem(activeItem.id);
57
- if (activeIndex != -1) items.splice(activeIndex, 1);
80
+ items.splice(activeIndex, 1);
58
81
  }}
59
82
  >
60
83
  <p>
61
- Are you sure you want to permanently delete
62
- {#if activeItem?.name}<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
63
- {:else}this
64
- {/if}?
84
+ Are you sure you want to permanently delete {@render _name()}?
65
85
  </p>
66
86
  </FormDialog>
67
87
 
68
88
  <style>
69
89
  .list-item {
70
- grid-template-columns: 1em 4fr 15em 5em 1em !important;
90
+ grid-template-columns: 1em 4fr 15em 5em 1em 1em;
71
91
  }
72
92
  </style>
@@ -4,14 +4,13 @@
4
4
  import { FormDialog, Icon, NumberBar } from '@axium/server/components';
5
5
  import { deleteItem, updateItemMetadata } from '@axium/storage/client';
6
6
  import type { StorageItemUpdate } from '@axium/storage/common';
7
+ import { StorageList } from '@axium/storage/components';
8
+ import '@axium/storage/styles/list';
7
9
 
8
10
  const { data } = $props();
9
- const {
10
- info: { limits },
11
- session,
12
- } = data;
11
+ const { limits } = data.info;
13
12
 
14
- const items = $state(data.info.items.filter(i => i.type != 'inode/directory').sort((a, b) => Math.sign(b.size - a.size)));
13
+ let items = $state(data.info.items.filter(i => i.type != 'inode/directory').sort((a, b) => Math.sign(b.size - a.size)));
15
14
  const usage = $state(data.info.usage);
16
15
 
17
16
  let dialogs = $state<Record<string, HTMLDialogElement>>({});
@@ -23,71 +22,13 @@
23
22
  </svelte:head>
24
23
 
25
24
  {#snippet action(name: string, i: string = 'pen')}
26
- <button style:display="contents" onclick={() => dialogs[name].showModal()}>
25
+ <span class="action" onclick={() => dialogs[name].showModal()}>
27
26
  <Icon {i} --size="16px" />
28
- </button>
27
+ </span>
29
28
  {/snippet}
30
29
 
31
- <div class="flex-content">
32
- <div class="list main">
33
- <h2>Storage Usage</h2>
30
+ <h2>Storage Usage</h2>
34
31
 
35
- <p><NumberBar max={limits.user_size * 1_000_000} value={usage?.bytes} text={barText} --fill="#345" /></p>
32
+ <p><NumberBar max={limits.user_size * 1_000_000} value={usage?.bytes} text={barText} --fill="#345" /></p>
36
33
 
37
- {#each items as item}
38
- <div class="item">
39
- <Icon i={forMime(item.type)} />
40
- <p>{item.name}</p>
41
- <p>{item.type}</p>
42
- <p>Owned by {item.userId === session?.userId ? 'You' : item.userId}</p>
43
- <p>{formatBytes(item.size)}</p>
44
- <p>Uploaded {item.modifiedAt.toLocaleString()}</p>
45
- <span>{@render action('rename#' + item.id)}</span>
46
- <span>{@render action('delete#' + item.id, 'trash')}</span>
47
- </div>
48
- <FormDialog
49
- bind:dialog={dialogs['rename#' + item.id]}
50
- submit={(data: StorageItemUpdate) => updateItemMetadata(item.id, data).then(n => (item.name = n.name))}
51
- submitText="Update"
52
- >
53
- <div>
54
- <label for="name">Name</label>
55
- <input name="name" type="text" value={item.name || ''} required />
56
- </div>
57
- </FormDialog>
58
- <FormDialog
59
- bind:dialog={dialogs['delete#' + item.id]}
60
- submit={async (data: StorageItemUpdate) => {
61
- await deleteItem(item.id);
62
- dialogs['delete#' + item.id].close();
63
- items.splice(items.indexOf(item), 1);
64
- }}
65
- submitText="Delete"
66
- submitDanger
67
- >
68
- <p>
69
- Are you sure you want to delete this file?<br />
70
- This action can't be undone.
71
- </p>
72
- </FormDialog>
73
- {/each}
74
- </div>
75
- </div>
76
-
77
- <style>
78
- .list {
79
- width: 80%;
80
- padding-top: 4em;
81
- }
82
-
83
- .item {
84
- display: grid;
85
- align-items: center;
86
- width: 100%;
87
- gap: 1em;
88
- text-wrap: nowrap;
89
- border-top: 1px solid #8888;
90
- padding-bottom: 1em;
91
- grid-template-columns: 2em 1.5fr 1fr 1fr 5em 1fr 2em 2em;
92
- }
93
- </style>
34
+ <StorageList bind:items emptyText="You have not uploaded any files yet." />
package/styles/list.css CHANGED
@@ -4,9 +4,9 @@
4
4
  padding: 0.5em;
5
5
  }
6
6
 
7
- .list-item.list-header {
7
+ .list-header {
8
8
  font-weight: bold;
9
- border-bottom: 1px solid #bbc;
9
+ border-bottom: 1.5px solid #bbc;
10
10
  }
11
11
 
12
12
  .list-item-container {
@@ -18,17 +18,20 @@
18
18
  display: grid;
19
19
  grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
20
20
  align-items: center;
21
- gap: 0.5em;
22
- padding: 0.5em 0;
21
+ gap: 1em;
22
+ padding: 0.5em;
23
+ overflow: hidden;
24
+ text-wrap: nowrap;
23
25
  }
24
26
 
25
- .list-item:not(:last-child) {
26
- border-bottom: 1px solid #bbc;
27
+ .list-item:not(.list-header, :first-child) {
28
+ border-top: 1px solid #bbc;
27
29
  }
28
30
 
29
31
  .list-item:not(.list-header):hover {
30
32
  background-color: #7777;
31
33
  }
34
+
32
35
  p.list-empty {
33
36
  text-align: center;
34
37
  color: #888;
@@ -40,10 +43,7 @@ p.list-empty {
40
43
  visibility: hidden;
41
44
  }
42
45
 
43
- .item:hover .action {
46
+ .list-item:hover .action {
44
47
  visibility: visible;
45
- }
46
-
47
- .action:hover {
48
48
  cursor: pointer;
49
49
  }