@axium/client 0.4.8 → 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 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
  }
@@ -1,12 +1,79 @@
1
1
  <script lang="ts">
2
2
  import FormDialog from './FormDialog.svelte';
3
- import AccessControl from './AccessControl.svelte';
3
+ import { permissionNames, type AccessControllable } from '@axium/core/access';
4
+ import type { Entries } from 'utilium';
5
+ import UserCard from './UserCard.svelte';
6
+ import type { Permission, AccessControl } from '@axium/core/access';
7
+ import type { User } from '@axium/core';
4
8
 
5
- let { item, editable } = $props();
9
+ let {
10
+ item = $bindable(),
11
+ editable,
12
+ dialog = $bindable(),
13
+ }: { item: Required<AccessControllable> & { name?: string; user: User }; editable: boolean; dialog?: HTMLDialogElement } = $props();
14
+
15
+ const permEntries = Object.entries(permissionNames) as any as Entries<typeof permissionNames>;
16
+
17
+ const publicPerm = $derived(permissionNames[item.publicPermission]);
6
18
  </script>
7
19
 
8
- <FormDialog submitText="Save">
20
+ <FormDialog bind:dialog submitText="Save">
21
+ {#snippet header()}
22
+ {#if item.name}
23
+ <h3>Permissions for <strong>{item.name}</strong></h3>
24
+ {:else}
25
+ <h3>Permissions</h3>
26
+ {/if}
27
+ {/snippet}
28
+
29
+ <div class="AccessControl">
30
+ <UserCard user={item.user} />
31
+ <span>Owner</span>
32
+ </div>
33
+
9
34
  {#each item.acl as control}
10
- <AccessControl {control} {editable} />
35
+ <div class="AccessControl">
36
+ {#if !control.user}<i>Unknown</i>
37
+ {:else}
38
+ <UserCard user={control.user} />
39
+ {#if editable}
40
+ <input type="hidden" name="userId" value={control.user.id} />
41
+ <select name="permission">
42
+ {#each permEntries as [key, name]}
43
+ <option value={key} selected={key == control.permission}>{name}</option>
44
+ {/each}
45
+ </select>
46
+ {:else}
47
+ <span>{permEntries[control.permission]}</span>
48
+ {/if}
49
+ {/if}
50
+ </div>
11
51
  {/each}
52
+
53
+ <div class="AccessControl public">
54
+ <strong>Public Access</strong>
55
+ {#if editable}
56
+ <select name="publicPermission">
57
+ {#each permEntries as [key, name]}
58
+ <option value={key} selected={key == item.publicPermission}>{name}</option>
59
+ {/each}
60
+ </select>
61
+ {:else}
62
+ <span>{publicPerm}</span>
63
+ {/if}
64
+ </div>
12
65
  </FormDialog>
66
+
67
+ <style>
68
+ .AccessControl {
69
+ display: grid;
70
+ gap: 1em;
71
+ grid-template-columns: 1fr 10em;
72
+ min-width: 30em;
73
+ padding: 1em 2em;
74
+
75
+ &:not(.public) {
76
+ border-bottom: 1px solid var(--border-accent);
77
+ }
78
+ }
79
+ </style>
@@ -3,7 +3,9 @@
3
3
  </script>
4
4
 
5
5
  <div class="Bar">
6
- <div class="fill" style="width: {((value - min) / (max - min)) * 100}%"></div>
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
@@ -1,4 +1,3 @@
1
- export { default as AccessControl } from './AccessControl.svelte';
2
1
  export { default as AccessControlDialog } from './AccessControlDialog.svelte';
3
2
  export { default as AppMenu } from './AppMenu.svelte';
4
3
  export { default as ClipboardCopy } from './ClipboardCopy.svelte';
@@ -12,7 +11,9 @@ export { default as Popover } from './Popover.svelte';
12
11
  export { default as Preference } from './Preference.svelte';
13
12
  export { default as Preferences } from './Preferences.svelte';
14
13
  export { default as Register } from './Register.svelte';
14
+ export { default as SessionList } from './SessionList.svelte';
15
15
  export { default as Toast } from './Toast.svelte';
16
16
  export { default as Upload } from './Upload.svelte';
17
17
  export { default as UserCard } from './UserCard.svelte';
18
+ export { default as UserMenu } from './UserMenu.svelte';
18
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.4.8",
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
+ }
@@ -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
+ }
@@ -1,37 +0,0 @@
1
- <script lang="ts">
2
- import UserCard from './UserCard.svelte';
3
- import type { Permission, AccessControl } from '@axium/core/access';
4
- import { permissionNames } from '@axium/core/access';
5
- import type { Entries } from 'utilium';
6
-
7
- const { control, editable }: { control: AccessControl; editable: boolean } = $props();
8
-
9
- const perm = $derived(permissionNames[control.permission as Permission]);
10
-
11
- const permEntries = Object.entries(permissionNames) as any as Entries<typeof permissionNames>;
12
- </script>
13
-
14
- <div class="AccessControl">
15
- {#if !control.user}<i>Unknown</i>
16
- {:else}
17
- <UserCard user={control.user} />
18
- {#if editable}
19
- <input type="hidden" name="userId" value={control.user.id} />
20
- <select name="permission">
21
- {#each permEntries as [key, name]}
22
- <option value={key} selected={key == control.permission}>{name}</option>
23
- {/each}
24
- </select>
25
- {:else}
26
- <span>{perm}</span>
27
- {/if}
28
- {/if}
29
- </div>
30
-
31
- <style>
32
- .AccessControl {
33
- display: flex;
34
- gap: 1em;
35
- padding: 1em 2em;
36
- }
37
- </style>