@axium/client 0.8.0 → 0.9.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/assets/styles.css CHANGED
@@ -151,7 +151,8 @@ pre {
151
151
  background-color: hsl(from var(--bg-menu) h s l / 95%);
152
152
  border-radius: 0.5em;
153
153
  padding: 0.25em;
154
- inset: unset;
154
+ margin: 0;
155
+ inset: auto;
155
156
  display: flex;
156
157
  flex-direction: column;
157
158
  gap: 0.1em;
@@ -166,3 +167,15 @@ pre {
166
167
  a:has(button, div) {
167
168
  display: contents;
168
169
  }
170
+
171
+ .version {
172
+ font-family: monospace;
173
+ font-size: 0.9em;
174
+ color: #aaa;
175
+ margin-left: 1em;
176
+ }
177
+
178
+ .version::before {
179
+ content: 'v';
180
+ color: #888;
181
+ }
package/dist/access.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { fetchAPI } from './requests.js';
2
2
  export async function setACL(itemType, itemId, data) {
3
- if ('public' in data)
4
- data.public = parseInt(data.public.toString());
5
3
  return await fetchAPI('POST', 'acl/:itemType/:itemId', data, itemType, itemId);
6
4
  }
7
5
  export async function getACL(itemType, itemId) {
@@ -1,11 +1,11 @@
1
1
  <script lang="ts">
2
+ import { getACL, setACL } from '@axium/client/access';
3
+ import { userInfo } from '@axium/client/user';
2
4
  import type { AccessMap, User } from '@axium/core';
3
- import { permissionNames, type AccessControllable, type AccessControl } from '@axium/core/access';
4
- import type { Entries } from 'utilium';
5
+ import { pickPermissions, type AccessControl, type AccessControllable } from '@axium/core/access';
5
6
  import FormDialog from './FormDialog.svelte';
6
7
  import UserCard from './UserCard.svelte';
7
- import { userInfo } from '@axium/client/user';
8
- import { getACL, setACL } from '@axium/client/access';
8
+ import Icon from './Icon.svelte';
9
9
 
10
10
  interface Props {
11
11
  editable: boolean;
@@ -17,10 +17,6 @@
17
17
  let { item = $bindable(), itemType, editable, dialog = $bindable(), acl = $bindable(item?.acl) }: Props = $props();
18
18
 
19
19
  if (!acl && item) getACL(itemType, item.id).then(fetched => (acl = item.acl = fetched));
20
-
21
- const permEntries = Object.entries(permissionNames) as any as Entries<typeof permissionNames>;
22
-
23
- const publicPerm = $derived(permissionNames[item?.publicPermission || 0]);
24
20
  </script>
25
21
 
26
22
  <FormDialog
@@ -49,34 +45,33 @@
49
45
 
50
46
  {#each acl ?? [] as control}
51
47
  <div class="AccessControl">
52
- {#if !control.user}<i>Unknown</i>
53
- {:else}
48
+ {#if control.user}
54
49
  <UserCard user={control.user} />
55
- {#if editable}
56
- <select name={control.userId}>
57
- {#each permEntries as [key, name]}
58
- <option value={key} selected={key == control.permission}>{name}</option>
59
- {/each}
60
- </select>
61
- {:else}
62
- <span>{permEntries[control.permission]}</span>
63
- {/if}
50
+ {:else if control.role}
51
+ <span class="icon-text">
52
+ <Icon i="at" />
53
+ <span>{control.role}</span>
54
+ </span>
55
+ <!-- {:else if control.tag} -->
56
+ {:else}
57
+ <i>Unknown</i>
58
+ {/if}
59
+ {#if editable}
60
+ <select name={control.userId ?? (control.role ? '@' + control.role : 'public')} multiple>
61
+ {#each Object.entries(pickPermissions(control)) as [key, value]}
62
+ <option value={key} selected={!!value}>{key}</option>
63
+ {/each}
64
+ </select>
65
+ {:else}
66
+ <span
67
+ >{Object.entries(pickPermissions(control))
68
+ .filter(([, value]) => value)
69
+ .map(([key]) => key)
70
+ .join(', ')}</span
71
+ >
64
72
  {/if}
65
73
  </div>
66
74
  {/each}
67
-
68
- <div class="AccessControl public">
69
- <strong>Public Access</strong>
70
- {#if editable && item}
71
- <select name="public">
72
- {#each permEntries as [key, name]}
73
- <option value={key} selected={key == item.publicPermission}>{name}</option>
74
- {/each}
75
- </select>
76
- {:else}
77
- <span>{publicPerm}</span>
78
- {/if}
79
- </div>
80
75
  </FormDialog>
81
76
 
82
77
  <style>
package/lib/Dialog.svelte CHANGED
@@ -1,5 +1,7 @@
1
- <script>
2
- let { children, dialog = $bindable(), ...rest } = $props();
1
+ <script lang="ts">
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
+
4
+ let { children, dialog = $bindable(), ...rest }: { children(): any; dialog?: HTMLDialogElement } & HTMLDialogAttributes = $props();
3
5
  </script>
4
6
 
5
7
  <dialog bind:this={dialog} {...rest}>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Dialog from './Dialog.svelte';
3
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
4
 
4
5
  let {
5
6
  children,
@@ -26,7 +27,7 @@
26
27
  submitDanger?: boolean;
27
28
  header?(): any;
28
29
  footer?(): any;
29
- } = $props();
30
+ } & HTMLDialogAttributes = $props();
30
31
 
31
32
  let error = $state<string>();
32
33
 
@@ -34,7 +35,7 @@
34
35
  if (pageMode) dialog!.showModal();
35
36
  });
36
37
 
37
- function onclose(e: MouseEvent) {
38
+ function onclose(e: Event) {
38
39
  e.preventDefault();
39
40
  cancel();
40
41
  }
package/lib/Login.svelte CHANGED
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { loginByEmail } from '@axium/client/user';
3
3
  import FormDialog from './FormDialog.svelte';
4
- import redirectAfter from './auth_redirect.js';
4
+ import authRedirect from './auth_redirect.js';
5
5
 
6
6
  let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
7
7
 
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  await loginByEmail(data.email);
14
+ const redirectAfter = await authRedirect();
14
15
  if (fullPage && redirectAfter) location.href = redirectAfter;
15
16
  }
16
17
  </script>
package/lib/Logout.svelte CHANGED
@@ -2,21 +2,22 @@
2
2
  import { logoutCurrentSession } from '@axium/client/user';
3
3
  import FormDialog from './FormDialog.svelte';
4
4
 
5
- let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
5
+ let { fullPage = false }: { fullPage?: boolean } = $props();
6
6
  </script>
7
7
 
8
8
  <FormDialog
9
9
  pageMode={fullPage}
10
- bind:dialog
10
+ id="logout"
11
11
  submitText="Log Out"
12
12
  submit={() => logoutCurrentSession().then(() => (window.location.href = '/'))}
13
13
  >
14
14
  <p>Are you sure you want to log out?</p>
15
15
  {#if fullPage}
16
16
  <button
17
+ command="close"
18
+ commandfor="logout"
17
19
  onclick={e => {
18
20
  e.preventDefault();
19
- dialog!.close();
20
21
  history.back();
21
22
  }}>Take me back</button
22
23
  >
@@ -6,7 +6,8 @@
6
6
 
7
7
  function onclick(e: MouseEvent) {
8
8
  e.stopPropagation();
9
- popover?.togglePopover();
9
+ // @ts-expect-error 2345
10
+ popover?.togglePopover({ source: e.currentTarget });
10
11
  }
11
12
  </script>
12
13
 
@@ -31,6 +32,11 @@
31
32
  cursor: pointer;
32
33
  }
33
34
 
35
+ .popover-toggle + [popover] {
36
+ position-area: bottom right;
37
+ position-try: most-width flip-inline;
38
+ }
39
+
34
40
  [popover] :global(.menu-item) {
35
41
  display: inline-flex;
36
42
  align-items: center;
@@ -1,12 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { register } from '@axium/client/user';
3
3
  import FormDialog from './FormDialog.svelte';
4
- import redirectAfter from './auth_redirect.js';
4
+ import authRedirect from './auth_redirect.js';
5
5
 
6
6
  let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
7
7
 
8
8
  async function submit(data: Record<string, FormDataEntryValue>) {
9
9
  await register(data);
10
+ const redirectAfter = await authRedirect();
10
11
  if (fullPage && redirectAfter) location.href = redirectAfter;
11
12
  }
12
13
  </script>
@@ -10,8 +10,6 @@
10
10
  user,
11
11
  redirectAfterLogoutAll = false,
12
12
  }: { sessions: Session[]; currentSession?: Session; user: User; redirectAfterLogoutAll?: boolean } = $props();
13
-
14
- const dialogs = $state<Record<string, HTMLDialogElement>>({});
15
13
  </script>
16
14
 
17
15
  {#each sessions as session}
@@ -27,15 +25,14 @@
27
25
  </p>
28
26
  <p>Created {session.created.toLocaleString()}</p>
29
27
  <p>Expires {session.expires.toLocaleString()}</p>
30
- <button style:display="contents" onclick={() => dialogs['logout#' + session.id].showModal()}>
28
+ <button style:display="contents" command="show-modal" commandfor={'logout-session:' + session.id}>
31
29
  <Icon i="right-from-bracket" --size="16px" />
32
30
  </button>
33
31
  </div>
34
32
  <FormDialog
35
- bind:dialog={dialogs['logout#' + session.id]}
33
+ id={'logout-session:' + session.id}
36
34
  submit={async () => {
37
35
  await logout(user.id, session.id);
38
- dialogs['logout#' + session.id].remove();
39
36
  sessions.splice(sessions.indexOf(session), 1);
40
37
  if (session.id == currentSession?.id) window.location.href = '/';
41
38
  }}
@@ -45,10 +42,10 @@
45
42
  </FormDialog>
46
43
  {/each}
47
44
  <span>
48
- <button onclick={() => dialogs.logout_all.showModal()} class="danger">Logout All</button>
45
+ <button command="show-modal" commandfor="logout-all" class="danger">Logout All</button>
49
46
  </span>
50
47
  <FormDialog
51
- bind:dialog={dialogs.logout_all}
48
+ id="logout-all"
52
49
  submit={() => logoutAll(user.id).then(() => (redirectAfterLogoutAll ? (window.location.href = '/') : null))}
53
50
  submitText="Logout All Sessions"
54
51
  submitDanger
@@ -7,8 +7,6 @@
7
7
  import Logout from './Logout.svelte';
8
8
 
9
9
  const { user }: { user: Partial<User> } = $props();
10
-
11
- let logout = $state<HTMLDialogElement>()!;
12
10
  </script>
13
11
 
14
12
  <Popover>
@@ -52,13 +50,15 @@
52
50
  <i>Couldn't load apps.</i>
53
51
  {/await}
54
52
 
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>
53
+ <button style:display="contents" command="show-modal" commandfor="logout">
54
+ <span class="menu-item logout">
55
+ <Icon i="right-from-bracket" --size="1.5em" --fill="hsl(0 33 var(--fg-light))" />
56
+ <span>Logout</span>
57
+ </span>
58
+ </button>
59
59
  </Popover>
60
60
 
61
- <Logout bind:dialog={logout} />
61
+ <Logout />
62
62
 
63
63
  <style>
64
64
  img {
@@ -2,8 +2,11 @@ import { getCurrentSession } from '@axium/client/user';
2
2
 
3
3
  function resolveRedirect(): string | false {
4
4
  const url = new URL(location.href);
5
+ if (!['/login', '/register'].includes(url.pathname)) {
6
+ console.warn('Not on login or register page, not redirecting:', url.pathname);
7
+ return false;
8
+ }
5
9
  const maybe = url.searchParams.get('after');
6
- if (!['/login', '/register'].includes(url.pathname)) return false;
7
10
  if (!maybe || maybe == url.pathname) return '/';
8
11
 
9
12
  if (maybe[0] != '/' || maybe[1] == '/') {
@@ -16,13 +19,13 @@ function resolveRedirect(): string | false {
16
19
  return redirect.pathname + redirect.search || '/';
17
20
  }
18
21
 
19
- const redirect = resolveRedirect();
22
+ export default async function authRedirect() {
23
+ const redirect = resolveRedirect();
20
24
 
21
- try {
22
- if (!redirect) throw 'No redirect';
23
- // Auto-redirect if already logged in.
24
- const session = await getCurrentSession();
25
- if (session) location.href = redirect;
26
- } catch {}
27
-
28
- export default redirect;
25
+ try {
26
+ if (!redirect) throw 'No redirect';
27
+ // Auto-redirect if already logged in.
28
+ const session = await getCurrentSession();
29
+ if (session) location.href = redirect;
30
+ } catch {}
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "author": "James Prevett <jp@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -40,7 +40,7 @@
40
40
  "build": "tsc"
41
41
  },
42
42
  "peerDependencies": {
43
- "@axium/core": ">=0.10.0",
43
+ "@axium/core": ">=0.12.0",
44
44
  "utilium": "^2.3.8",
45
45
  "zod": "^4.0.5",
46
46
  "svelte": "^5.36.0"