@axium/client 0.4.9 → 0.5.0
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/assets/styles.css +7 -0
- package/dist/requests.js +4 -1
- package/lib/NumberBar.svelte +3 -1
- package/lib/SessionList.svelte +57 -0
- package/lib/UserMenu.svelte +75 -0
- package/lib/index.ts +2 -0
- package/package.json +5 -3
- package/styles/account.css +57 -0
- package/styles/list.css +61 -0
package/assets/styles.css
CHANGED
|
@@ -73,6 +73,13 @@ button:hover {
|
|
|
73
73
|
background-color: hsl(var(--hue) 15 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
code,
|
|
77
|
+
pre {
|
|
78
|
+
background-color: var(--bg-menu);
|
|
79
|
+
padding: 1em;
|
|
80
|
+
border-radius: 0.5em;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
.error {
|
|
77
84
|
padding: 1em;
|
|
78
85
|
border-radius: 0.5em;
|
package/dist/requests.js
CHANGED
|
@@ -16,6 +16,9 @@ export async function fetchAPI(method, endpoint, data, ...params) {
|
|
|
16
16
|
};
|
|
17
17
|
if (method !== 'GET' && method !== 'HEAD')
|
|
18
18
|
options.body = JSON.stringify(data);
|
|
19
|
+
const search = method != 'GET' || typeof data != 'object' || data == null || !Object.keys(data).length
|
|
20
|
+
? ''
|
|
21
|
+
: '?' + new URLSearchParams(data).toString();
|
|
19
22
|
if (token)
|
|
20
23
|
options.headers.Authorization = 'Bearer ' + token;
|
|
21
24
|
const parts = [];
|
|
@@ -29,7 +32,7 @@ export async function fetchAPI(method, endpoint, data, ...params) {
|
|
|
29
32
|
throw new Error(`Missing parameter "${part.slice(1)}"`);
|
|
30
33
|
parts.push(value);
|
|
31
34
|
}
|
|
32
|
-
const response = await fetch(prefix + parts.join('/'), options);
|
|
35
|
+
const response = await fetch(prefix + parts.join('/') + search, options);
|
|
33
36
|
if (!response.headers.get('Content-Type')?.includes('application/json')) {
|
|
34
37
|
throw new Error(`Unexpected response type: ${response.headers.get('Content-Type')}`);
|
|
35
38
|
}
|
package/lib/NumberBar.svelte
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<div class="Bar">
|
|
6
|
-
|
|
6
|
+
{#if max}
|
|
7
|
+
<div class="fill" style="width: {((value - min) / (max - min)) * 100}%"></div>
|
|
8
|
+
{/if}
|
|
7
9
|
{#if text}<span class="text">{text}</span>{/if}
|
|
8
10
|
</div>
|
|
9
11
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { logout, logoutAll } from '@axium/client/user';
|
|
3
|
+
import type { Session, User } from '@axium/core';
|
|
4
|
+
import FormDialog from './FormDialog.svelte';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
sessions = $bindable(),
|
|
9
|
+
currentSession,
|
|
10
|
+
user,
|
|
11
|
+
redirectAfterLogoutAll = false,
|
|
12
|
+
}: { sessions: Session[]; currentSession?: Session; user: User; redirectAfterLogoutAll?: boolean } = $props();
|
|
13
|
+
|
|
14
|
+
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#each sessions as session}
|
|
18
|
+
<div class="item session">
|
|
19
|
+
<p>
|
|
20
|
+
{session.id.slice(0, 4)}...{session.id.slice(-4)}
|
|
21
|
+
{#if session.id == currentSession?.id}
|
|
22
|
+
<span class="current">Current</span>
|
|
23
|
+
{/if}
|
|
24
|
+
{#if session.elevated}
|
|
25
|
+
<span class="elevated">Elevated</span>
|
|
26
|
+
{/if}
|
|
27
|
+
</p>
|
|
28
|
+
<p>Created {session.created.toLocaleString()}</p>
|
|
29
|
+
<p>Expires {session.expires.toLocaleString()}</p>
|
|
30
|
+
<button style:display="contents" onclick={() => dialogs['logout#' + session.id].showModal()}>
|
|
31
|
+
<Icon i="right-from-bracket" --size="16px" />
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<FormDialog
|
|
35
|
+
bind:dialog={dialogs['logout#' + session.id]}
|
|
36
|
+
submit={async () => {
|
|
37
|
+
await logout(user.id, session.id);
|
|
38
|
+
dialogs['logout#' + session.id].remove();
|
|
39
|
+
sessions.splice(sessions.indexOf(session), 1);
|
|
40
|
+
if (session.id == currentSession?.id) window.location.href = '/';
|
|
41
|
+
}}
|
|
42
|
+
submitText="Logout"
|
|
43
|
+
>
|
|
44
|
+
<p>Are you sure you want to log out this session?</p>
|
|
45
|
+
</FormDialog>
|
|
46
|
+
{/each}
|
|
47
|
+
<span>
|
|
48
|
+
<button onclick={() => dialogs.logout_all.showModal()} class="danger">Logout All</button>
|
|
49
|
+
</span>
|
|
50
|
+
<FormDialog
|
|
51
|
+
bind:dialog={dialogs.logout_all}
|
|
52
|
+
submit={() => logoutAll(user.id).then(() => (redirectAfterLogoutAll ? (window.location.href = '/') : null))}
|
|
53
|
+
submitText="Logout All Sessions"
|
|
54
|
+
submitDanger
|
|
55
|
+
>
|
|
56
|
+
<p>Are you sure you want to log out all sessions?</p>
|
|
57
|
+
</FormDialog>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { User } from '@axium/core/user';
|
|
3
|
+
import { getUserImage } from '@axium/core';
|
|
4
|
+
import { fetchAPI } from '@axium/client/requests';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
import Popover from './Popover.svelte';
|
|
7
|
+
import Logout from './Logout.svelte';
|
|
8
|
+
|
|
9
|
+
const { user }: { user: Partial<User> } = $props();
|
|
10
|
+
|
|
11
|
+
let logout = $state<HTMLDialogElement>()!;
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<Popover>
|
|
15
|
+
{#snippet toggle()}
|
|
16
|
+
<div style:display="contents">
|
|
17
|
+
<img src={getUserImage(user)} alt={user.name} />
|
|
18
|
+
{user.name}
|
|
19
|
+
</div>
|
|
20
|
+
{/snippet}
|
|
21
|
+
|
|
22
|
+
<a class="menu-item" href="/account">
|
|
23
|
+
<Icon i="user" --size="1.5em" />
|
|
24
|
+
<span>Your Account</span>
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
{#if user.isAdmin}
|
|
28
|
+
<a class="menu-item" href="/admin">
|
|
29
|
+
<Icon i="gear-complex" --size="1.5em" />
|
|
30
|
+
<span>Administration</span>
|
|
31
|
+
</a>
|
|
32
|
+
{/if}
|
|
33
|
+
|
|
34
|
+
{#await fetchAPI('GET', 'apps')}
|
|
35
|
+
<i>Loading...</i>
|
|
36
|
+
{:then apps}
|
|
37
|
+
{#each apps as app}
|
|
38
|
+
<a class="menu-item" href="/{app.id}">
|
|
39
|
+
{#if app.image}
|
|
40
|
+
<img src={app.image} alt={app.name} width="1em" height="1em" />
|
|
41
|
+
{:else if app.icon}
|
|
42
|
+
<Icon i={app.icon} --size="1.5em" />
|
|
43
|
+
{:else}
|
|
44
|
+
<Icon i="image-circle-xmark" --size="1.5em" />
|
|
45
|
+
{/if}
|
|
46
|
+
<span>{app.name}</span>
|
|
47
|
+
</a>
|
|
48
|
+
{:else}
|
|
49
|
+
<i>No apps available.</i>
|
|
50
|
+
{/each}
|
|
51
|
+
{:catch}
|
|
52
|
+
<i>Couldn't load apps.</i>
|
|
53
|
+
{/await}
|
|
54
|
+
|
|
55
|
+
<span class="menu-item logout" onclick={() => logout.showModal()}>
|
|
56
|
+
<Icon i="right-from-bracket" --size="1.5em" --fill="hsl(0 33 var(--fg-light))" />
|
|
57
|
+
<span>Logout</span>
|
|
58
|
+
</span>
|
|
59
|
+
</Popover>
|
|
60
|
+
|
|
61
|
+
<Logout bind:dialog={logout} />
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
img {
|
|
65
|
+
width: 2em;
|
|
66
|
+
height: 2em;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
vertical-align: middle;
|
|
69
|
+
margin-right: 0.5em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
span.logout > span {
|
|
73
|
+
color: hsl(0 33 var(--fg-light));
|
|
74
|
+
}
|
|
75
|
+
</style>
|
package/lib/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ export { default as Popover } from './Popover.svelte';
|
|
|
11
11
|
export { default as Preference } from './Preference.svelte';
|
|
12
12
|
export { default as Preferences } from './Preferences.svelte';
|
|
13
13
|
export { default as Register } from './Register.svelte';
|
|
14
|
+
export { default as SessionList } from './SessionList.svelte';
|
|
14
15
|
export { default as Toast } from './Toast.svelte';
|
|
15
16
|
export { default as Upload } from './Upload.svelte';
|
|
16
17
|
export { default as UserCard } from './UserCard.svelte';
|
|
18
|
+
export { default as UserMenu } from './UserMenu.svelte';
|
|
17
19
|
export { default as WithContextMenu } from './WithContextMenu.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"assets",
|
|
23
23
|
"dist",
|
|
24
|
-
"lib"
|
|
24
|
+
"lib",
|
|
25
|
+
"styles"
|
|
25
26
|
],
|
|
26
27
|
"exports": {
|
|
27
28
|
".": "./dist/index.js",
|
|
28
29
|
"./*": "./dist/*.js",
|
|
29
30
|
"./components": "./lib/index.js",
|
|
30
|
-
"./components/*": "./lib/*.svelte"
|
|
31
|
+
"./components/*": "./lib/*.svelte",
|
|
32
|
+
"./styles/*": "./styles/*.css"
|
|
31
33
|
},
|
|
32
34
|
"scripts": {
|
|
33
35
|
"build": "tsc"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.section {
|
|
2
|
+
width: 50%;
|
|
3
|
+
padding-top: 4em;
|
|
4
|
+
|
|
5
|
+
/* This is causing duplicate separators when removing sessions/passkeys
|
|
6
|
+
> div:has(+ div) {
|
|
7
|
+
border-bottom: 1px solid #8888;
|
|
8
|
+
}
|
|
9
|
+
*/
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.section .item {
|
|
13
|
+
display: grid;
|
|
14
|
+
align-items: center;
|
|
15
|
+
width: 100%;
|
|
16
|
+
gap: 1em;
|
|
17
|
+
text-wrap: nowrap;
|
|
18
|
+
border-top: 1px solid #8888;
|
|
19
|
+
padding-bottom: 1em;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.info {
|
|
23
|
+
grid-template-columns: 10em 1fr 2em;
|
|
24
|
+
|
|
25
|
+
> :first-child {
|
|
26
|
+
margin-left: 1em;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
> :nth-child(2) {
|
|
30
|
+
text-overflow: ellipsis;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.passkey {
|
|
36
|
+
grid-template-columns: 1em 1em 1fr 1fr 1em 1em;
|
|
37
|
+
|
|
38
|
+
dfn:not(.disabled) {
|
|
39
|
+
cursor: help;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.session {
|
|
44
|
+
grid-template-columns: 1fr 1fr 1fr 1em;
|
|
45
|
+
|
|
46
|
+
.current {
|
|
47
|
+
border-radius: 2em;
|
|
48
|
+
padding: 0 0.5em;
|
|
49
|
+
background-color: #337;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.elevated {
|
|
53
|
+
border-radius: 2em;
|
|
54
|
+
padding: 0 0.5em;
|
|
55
|
+
background-color: #733;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/styles/list.css
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.list-container {
|
|
2
|
+
overflow-x: hidden;
|
|
3
|
+
overflow-y: scroll;
|
|
4
|
+
|
|
5
|
+
.list {
|
|
6
|
+
height: 100%;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.list {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
padding: 0.5em;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.list-header {
|
|
17
|
+
font-weight: bold;
|
|
18
|
+
border-bottom: 1.5px solid var(--fg-accent);
|
|
19
|
+
position: sticky;
|
|
20
|
+
top: 0em;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.list-item-container {
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
color: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.list-item {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 1em;
|
|
33
|
+
padding: 0.5em;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
text-wrap: nowrap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.list-item:not(.list-header, :first-child) {
|
|
39
|
+
border-top: 1px solid var(--fg-accent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.list-item:not(.list-header):hover {
|
|
43
|
+
background-color: var(--bg-alt);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
p.list-empty {
|
|
48
|
+
text-align: center;
|
|
49
|
+
color: #888;
|
|
50
|
+
margin-top: 1em;
|
|
51
|
+
font-style: italic;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.list-item .action {
|
|
55
|
+
visibility: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.list-item:hover .action {
|
|
59
|
+
visibility: visible;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|