@axium/storage 0.6.0 → 0.6.2
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/Add.svelte +93 -0
- package/lib/List.svelte +13 -11
- package/lib/Usage.svelte +10 -8
- package/lib/index.ts +1 -0
- package/package.json +2 -2
- package/routes/files/+layout.svelte +2 -6
- package/routes/files/+layout.ts +1 -4
- package/routes/files/+page.svelte +5 -4
- package/routes/files/[id]/+page.svelte +4 -2
- package/routes/files/trash/+page.svelte +40 -20
- package/routes/files/usage/+page.svelte +9 -68
- package/styles/list.css +10 -10
package/lib/Add.svelte
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { forMime } from '@axium/core/icons';
|
|
3
|
+
import { FormDialog, Icon, Popover, Upload } from '@axium/server/components';
|
|
4
|
+
import { uploadItem } from '@axium/storage/client';
|
|
5
|
+
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
|
+
|
|
7
|
+
const { parentId, onadd }: { parentId?: string; onadd?(item: StorageItemMetadata): void } = $props();
|
|
8
|
+
|
|
9
|
+
let uploadDialog = $state<HTMLDialogElement>()!;
|
|
10
|
+
let input = $state<HTMLInputElement>();
|
|
11
|
+
|
|
12
|
+
let createDialog = $state<HTMLDialogElement>()!;
|
|
13
|
+
let createType = $state<string>();
|
|
14
|
+
let createIncludesContent = $state(false);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#snippet _item(type: string, text: string, includeContent: boolean = false)}
|
|
18
|
+
<span
|
|
19
|
+
class="icon-text add-menu-item"
|
|
20
|
+
onclick={() => {
|
|
21
|
+
createType = type;
|
|
22
|
+
createIncludesContent = includeContent;
|
|
23
|
+
createDialog.showModal();
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<Icon i={forMime(type)} />
|
|
27
|
+
{text}
|
|
28
|
+
</span>
|
|
29
|
+
{/snippet}
|
|
30
|
+
|
|
31
|
+
<Popover>
|
|
32
|
+
{#snippet toggle()}
|
|
33
|
+
<button class="icon-text"><Icon i="plus" />Add</button>
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
<div class="add-menu">
|
|
37
|
+
<span class="icon-text add-menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />Upload</span>
|
|
38
|
+
{@render _item('inode/directory', 'New Folder')}
|
|
39
|
+
{@render _item('text/plain', 'Plain Text')}
|
|
40
|
+
{@render _item('text/x-uri', 'URL', true)}
|
|
41
|
+
</div>
|
|
42
|
+
</Popover>
|
|
43
|
+
|
|
44
|
+
<FormDialog
|
|
45
|
+
bind:dialog={uploadDialog}
|
|
46
|
+
submitText="Upload"
|
|
47
|
+
submit={async () => {
|
|
48
|
+
for (const file of input?.files!) {
|
|
49
|
+
const item = await uploadItem(file, { parentId });
|
|
50
|
+
onadd?.(item);
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Upload bind:input multiple />
|
|
55
|
+
</FormDialog>
|
|
56
|
+
|
|
57
|
+
<FormDialog
|
|
58
|
+
bind:dialog={createDialog}
|
|
59
|
+
submitText="Create"
|
|
60
|
+
submit={async (data: { name: string; content?: string }) => {
|
|
61
|
+
const file = new File(createIncludesContent ? [data.content!] : [], data.name, { type: createType });
|
|
62
|
+
const item = await uploadItem(file, { parentId });
|
|
63
|
+
onadd?.(item);
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<div>
|
|
67
|
+
<label for="name">Name</label>
|
|
68
|
+
<input name="name" type="text" required />
|
|
69
|
+
</div>
|
|
70
|
+
{#if createIncludesContent}
|
|
71
|
+
<div>
|
|
72
|
+
<label for="content">Content</label>
|
|
73
|
+
<input name="content" type="text" size="40" required />
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
</FormDialog>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.add-menu {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.add-menu-item {
|
|
85
|
+
border-radius: 0.5em;
|
|
86
|
+
padding: 0.25em 0.25em;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.add-menu-item:hover {
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
background-color: #4455;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
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 {
|
|
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">
|
|
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
|
-
|
|
73
|
-
activeItem
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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.
|
|
43
|
+
"@axium/server": ">=0.20.2",
|
|
44
44
|
"@sveltejs/kit": "^2.27.3",
|
|
45
45
|
"utilium": "^2.3.8"
|
|
46
46
|
},
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
import { Icon } from '@axium/server/components';
|
|
3
3
|
import { StorageUsage } from '@axium/storage/components';
|
|
4
4
|
import { capitalize } from 'utilium';
|
|
5
|
-
import type { LayoutProps } from './$types';
|
|
6
5
|
|
|
7
|
-
let { children, data }
|
|
6
|
+
let { children, data } = $props();
|
|
8
7
|
</script>
|
|
9
8
|
|
|
10
9
|
<div class="app">
|
|
11
10
|
<div class="sidebar">
|
|
12
11
|
{#each data.tabs as { href, name, icon: i, active }}
|
|
13
|
-
<a {href} class={['item', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
|
|
12
|
+
<a {href} class={['item', 'icon-text', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
|
|
14
13
|
{/each}
|
|
15
14
|
|
|
16
15
|
<div class="usage">
|
|
@@ -42,9 +41,6 @@
|
|
|
42
41
|
.item {
|
|
43
42
|
padding: 0.3em 0.5em;
|
|
44
43
|
border-radius: 0.25em 1em 1em 0.25em;
|
|
45
|
-
display: inline-flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
gap: 1em;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
.item:hover {
|
package/routes/files/+layout.ts
CHANGED
|
@@ -15,8 +15,5 @@ export async function load({ url, route }: LayoutLoadEvent) {
|
|
|
15
15
|
{ name: 'shared', href: '/files/shared', icon: 'user-group', active: route.id.endsWith('/files/shared') },
|
|
16
16
|
] satisfies { name: string; href: LayoutRouteId; icon: string; active: boolean }[];
|
|
17
17
|
|
|
18
|
-
return {
|
|
19
|
-
session: await getCurrentSession(),
|
|
20
|
-
tabs,
|
|
21
|
-
};
|
|
18
|
+
return { session, tabs };
|
|
22
19
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { StorageList } from '@axium/storage/components';
|
|
3
|
-
import type { PageProps } from './$types';
|
|
2
|
+
import { StorageAdd, StorageList } from '@axium/storage/components';
|
|
4
3
|
|
|
5
|
-
const { data }
|
|
4
|
+
const { data } = $props();
|
|
5
|
+
let items = $state(data.items!);
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<svelte:head>
|
|
9
9
|
<title>Files</title>
|
|
10
10
|
</svelte:head>
|
|
11
11
|
|
|
12
|
-
<StorageList appMode bind:items
|
|
12
|
+
<StorageList appMode bind:items />
|
|
13
|
+
<StorageAdd onadd={item => items.push(item)} />
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Icon } from '@axium/server/components';
|
|
3
|
-
import { StorageList } from '@axium/storage/components';
|
|
3
|
+
import { StorageAdd, StorageList } from '@axium/storage/components';
|
|
4
4
|
import type { PageProps } from './$types';
|
|
5
5
|
import { updateItemMetadata } from '@axium/storage/client';
|
|
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,8 @@
|
|
|
24
25
|
<Icon i="trash-can-undo" /> Restore
|
|
25
26
|
</button>
|
|
26
27
|
{:else if item.type == 'inode/directory'}
|
|
27
|
-
<StorageList appMode bind:items
|
|
28
|
+
<StorageList appMode bind:items />
|
|
29
|
+
<StorageAdd parentId={item.id} onadd={item => items.push(item)} />
|
|
28
30
|
{:else}
|
|
29
31
|
<p>No preview available.</p>
|
|
30
32
|
{/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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
25
|
+
<span class="action" onclick={() => dialogs[name].showModal()}>
|
|
27
26
|
<Icon {i} --size="16px" />
|
|
28
|
-
</
|
|
27
|
+
</span>
|
|
29
28
|
{/snippet}
|
|
30
29
|
|
|
31
|
-
<
|
|
32
|
-
<div class="list main">
|
|
33
|
-
<h2>Storage Usage</h2>
|
|
30
|
+
<h2>Storage Usage</h2>
|
|
34
31
|
|
|
35
|
-
|
|
32
|
+
<p><NumberBar max={limits.user_size * 1_000_000} value={usage?.bytes} text={barText} --fill="#345" /></p>
|
|
36
33
|
|
|
37
|
-
|
|
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-
|
|
7
|
+
.list-header {
|
|
8
8
|
font-weight: bold;
|
|
9
|
-
border-bottom:
|
|
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:
|
|
22
|
-
padding: 0.5em
|
|
21
|
+
gap: 1em;
|
|
22
|
+
padding: 0.5em;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
text-wrap: nowrap;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
.list-item:not(:
|
|
26
|
-
border-
|
|
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
|
}
|