@axium/client 0.10.0 → 0.12.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
@@ -101,7 +101,54 @@ pre {
101
101
  border-radius: 0.5em;
102
102
  }
103
103
 
104
- .error {
104
+ dialog {
105
+ border-radius: 1em;
106
+ background: var(--bg-menu);
107
+ border: 1px solid #8888;
108
+ padding: 1em;
109
+ max-width: calc(100% - 2em);
110
+ word-wrap: normal;
111
+
112
+ form {
113
+ max-width: 100%;
114
+ }
115
+ }
116
+
117
+ dialog::backdrop {
118
+ background: #0003;
119
+ }
120
+
121
+ dialog[open] {
122
+ animation: zoom 0.25s cubic-bezier(0.35, 1.55, 0.65, 1);
123
+ }
124
+
125
+ @keyframes zoom {
126
+ from {
127
+ transform: scale(0.95);
128
+ }
129
+ to {
130
+ transform: scale(1);
131
+ }
132
+ }
133
+
134
+ dialog[open]::backdrop {
135
+ animation: fade 0.25s ease-out;
136
+ }
137
+
138
+ @keyframes fade {
139
+ from {
140
+ opacity: 0;
141
+ }
142
+ to {
143
+ opacity: 1;
144
+ }
145
+ }
146
+
147
+ dialog form {
148
+ display: contents;
149
+ }
150
+
151
+ :not(input).error {
105
152
  padding: 1em;
106
153
  border-radius: 0.5em;
107
154
  background-color: var(--bg-error);
@@ -111,6 +158,10 @@ pre {
111
158
  color: hsl(0 50 50%);
112
159
  }
113
160
 
161
+ input.error {
162
+ border: 1px solid var(--bg-error);
163
+ }
164
+
114
165
  .success {
115
166
  padding: 1em;
116
167
  border-radius: 0.5em;
@@ -208,6 +259,9 @@ h6 {
208
259
  }
209
260
 
210
261
  @media (width < 700px) {
262
+ dialog {
263
+ }
264
+
211
265
  .mobile-hide {
212
266
  display: none !important;
213
267
  }
@@ -14,8 +14,10 @@ export declare function session(): {
14
14
  debug: boolean;
15
15
  };
16
16
  roles: string[];
17
+ tags: string[];
17
18
  registeredAt: Date;
18
19
  isAdmin: boolean;
20
+ isSuspended: boolean;
19
21
  emailVerified?: Date | null | undefined;
20
22
  image?: string | null | undefined;
21
23
  };
package/dist/config.d.ts CHANGED
@@ -14,14 +14,16 @@ export declare const ClientConfig: z.ZodObject<{
14
14
  id: z.ZodUUID;
15
15
  name: z.ZodString;
16
16
  email: z.ZodEmail;
17
- emailVerified: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
17
+ emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
18
18
  image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
19
- preferences: z.ZodObject<{
19
+ preferences: z.ZodLazy<z.ZodObject<{
20
20
  debug: z.ZodDefault<z.ZodBoolean>;
21
- }, z.core.$strip>;
21
+ }, z.core.$strip>>;
22
22
  roles: z.ZodArray<z.ZodString>;
23
+ tags: z.ZodArray<z.ZodString>;
23
24
  registeredAt: z.ZodCoercedDate<unknown>;
24
25
  isAdmin: z.ZodBoolean;
26
+ isSuspended: z.ZodBoolean;
25
27
  }, z.core.$strip>;
26
28
  }, z.core.$strip>;
27
29
  apps: z.ZodArray<z.ZodObject<{
@@ -1,7 +1,8 @@
1
- import type { $API, APIParameters, Endpoint, RequestBody } from '@axium/core/api';
1
+ import type { APIParameters, APIValues, Endpoint, RequestBody } from '@axium/core/api';
2
+ import { $API } from '@axium/core/api';
2
3
  import type { RequestMethod } from '@axium/core/requests';
3
4
  export declare let token: string | null;
4
5
  export declare function setToken(value: string | null): void;
5
6
  export declare let prefix: string;
6
7
  export declare function setPrefix(value: string): void;
7
- export declare function fetchAPI<const M extends RequestMethod, const E extends Endpoint>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<M extends keyof $API[E] ? ($API[E][M] extends [unknown, infer R] ? R : $API[E][M]) : unknown>;
8
+ export declare function fetchAPI<const E extends Endpoint, const M extends keyof $API[E] & RequestMethod>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<M extends keyof APIValues[E] ? (APIValues[E][M] extends readonly [unknown, infer R] ? R : APIValues[E][M]) : unknown>;
package/dist/requests.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { $API } from '@axium/core/api';
2
+ import { prettifyError } from 'zod';
1
3
  export let token = null;
2
4
  export function setToken(value) {
3
5
  token = value;
@@ -14,6 +16,14 @@ export async function fetchAPI(method, endpoint, data, ...params) {
14
16
  Accept: 'application/json',
15
17
  },
16
18
  };
19
+ const schema = $API[endpoint]?.[method];
20
+ if (schema && Array.isArray(schema))
21
+ try {
22
+ data = schema[0].parse(data);
23
+ }
24
+ catch (e) {
25
+ throw prettifyError(e);
26
+ }
17
27
  if (method !== 'GET' && method !== 'HEAD')
18
28
  options.body = JSON.stringify(data);
19
29
  const search = method != 'GET' || typeof data != 'object' || data == null || !Object.keys(data).length
@@ -39,5 +49,13 @@ export async function fetchAPI(method, endpoint, data, ...params) {
39
49
  const json = await response.json().catch(() => ({ message: 'Unknown server error (invalid JSON response)' }));
40
50
  if (!response.ok)
41
51
  throw new Error(json.message);
42
- return json;
52
+ if (!schema)
53
+ return json;
54
+ const Output = Array.isArray(schema) ? schema[1] : schema;
55
+ try {
56
+ return Output.parse(json);
57
+ }
58
+ catch (e) {
59
+ throw prettifyError(e);
60
+ }
43
61
  }
package/dist/user.d.ts CHANGED
@@ -19,7 +19,11 @@ export declare function updateUser(userId: string, data: Record<string, FormData
19
19
  export declare function fullUserInfo(userId: string): Promise<User & {
20
20
  sessions: Session[];
21
21
  }>;
22
- export declare function deleteUser(userId: string): Promise<User>;
22
+ /**
23
+ * @param userId The UUID of the user to delete
24
+ * @param deletingId The UUID of the user performing the deletion (for authentication). Defaults to userId.
25
+ */
26
+ export declare function deleteUser(userId: string, deletingId?: string): Promise<User>;
23
27
  export declare function emailVerificationEnabled(userId: string): Promise<boolean>;
24
28
  export declare function sendVerificationEmail(userId: string): Promise<Verification>;
25
29
  export declare function verifyEmail(userId: string, token: string): Promise<void>;
package/dist/user.js CHANGED
@@ -1,4 +1,3 @@
1
- import { UserChangeable } from '@axium/core';
2
1
  import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
3
2
  import * as z from 'zod';
4
3
  import { fetchAPI } from './requests.js';
@@ -23,38 +22,20 @@ export async function loginByEmail(email) {
23
22
  return await login(userId);
24
23
  }
25
24
  export async function getCurrentSession() {
26
- const result = await fetchAPI('GET', 'session');
27
- result.created = new Date(result.created);
28
- result.expires = new Date(result.expires);
29
- return result;
25
+ return await fetchAPI('GET', 'session');
30
26
  }
31
27
  export async function getSessions(userId) {
32
28
  _checkId(userId);
33
- const result = await fetchAPI('GET', 'users/:id/sessions', {}, userId);
34
- for (const session of result) {
35
- session.created = new Date(session.created);
36
- session.expires = new Date(session.expires);
37
- }
38
- return result;
29
+ return await fetchAPI('GET', 'users/:id/sessions', {}, userId);
39
30
  }
40
31
  export async function logout(userId, ...sessionId) {
41
32
  _checkId(userId);
42
- const result = await fetchAPI('DELETE', 'users/:id/sessions', { id: sessionId }, userId);
43
- for (const session of result) {
44
- session.created = new Date(session.created);
45
- session.expires = new Date(session.expires);
46
- }
47
- return result;
33
+ return await fetchAPI('DELETE', 'users/:id/sessions', { id: sessionId }, userId);
48
34
  }
49
35
  export async function logoutAll(userId) {
50
36
  _checkId(userId);
51
37
  await elevate(userId);
52
- const result = await fetchAPI('DELETE', 'users/:id/sessions', { confirm_all: true }, userId);
53
- for (const session of result) {
54
- session.created = new Date(session.created);
55
- session.expires = new Date(session.expires);
56
- }
57
- return result;
38
+ return await fetchAPI('DELETE', 'users/:id/sessions', { confirm_all: true }, userId);
58
39
  }
59
40
  export async function logoutCurrentSession() {
60
41
  return await fetchAPI('DELETE', 'session');
@@ -80,59 +61,43 @@ function _checkId(userId) {
80
61
  }
81
62
  export async function userInfo(userId) {
82
63
  _checkId(userId);
83
- const user = await fetchAPI('GET', 'users/:id', {}, userId);
84
- user.registeredAt = new Date(user.registeredAt);
85
- user.emailVerified = user.emailVerified ? new Date(user.emailVerified) : null;
86
- return user;
64
+ return await fetchAPI('GET', 'users/:id', {}, userId);
87
65
  }
88
66
  export async function updateUser(userId, data) {
89
67
  _checkId(userId);
90
- const body = await UserChangeable.parseAsync(data).catch(e => {
91
- throw e instanceof z.core.$ZodError ? z.prettifyError(e) : e;
92
- });
93
- const result = await fetchAPI('PATCH', 'users/:id', body, userId);
94
- result.registeredAt = new Date(result.registeredAt);
95
- if (result.emailVerified)
96
- result.emailVerified = new Date(result.emailVerified);
97
- return result;
68
+ return await fetchAPI('PATCH', 'users/:id', data, userId);
98
69
  }
99
70
  export async function fullUserInfo(userId) {
100
71
  _checkId(userId);
101
- const result = await fetchAPI('GET', 'users/:id/full', {}, userId);
102
- result.registeredAt = new Date(result.registeredAt);
103
- result.emailVerified = new Date(result.emailVerified);
104
- return result;
72
+ return await fetchAPI('GET', 'users/:id/full', {}, userId);
105
73
  }
106
- export async function deleteUser(userId) {
74
+ /**
75
+ * @param userId The UUID of the user to delete
76
+ * @param deletingId The UUID of the user performing the deletion (for authentication). Defaults to userId.
77
+ */
78
+ export async function deleteUser(userId, deletingId = userId) {
107
79
  _checkId(userId);
108
- const options = await fetchAPI('OPTIONS', 'users/:id/auth', { type: 'action' }, userId);
80
+ const options = await fetchAPI('OPTIONS', 'users/:id/auth', { type: 'action' }, deletingId);
109
81
  const response = await startAuthentication({ optionsJSON: options });
110
- await fetchAPI('POST', 'users/:id/auth', response, userId);
111
- const result = await fetchAPI('DELETE', 'users/:id', response, userId);
112
- result.registeredAt = new Date(result.registeredAt);
113
- result.emailVerified = new Date(result.emailVerified);
114
- return result;
82
+ await fetchAPI('POST', 'users/:id/auth', response, deletingId);
83
+ return await fetchAPI('DELETE', 'users/:id', response, userId);
115
84
  }
116
85
  export async function emailVerificationEnabled(userId) {
117
86
  _checkId(userId);
118
- const { enabled } = await fetchAPI('OPTIONS', 'users/:id/verify_email', {}, userId);
87
+ const { enabled } = await fetchAPI('OPTIONS', 'users/:id/verify/email', {}, userId);
119
88
  return enabled;
120
89
  }
121
90
  export async function sendVerificationEmail(userId) {
122
91
  _checkId(userId);
123
- return await fetchAPI('GET', 'users/:id/verify_email', {}, userId);
92
+ return await fetchAPI('GET', 'users/:id/verify/email', {}, userId);
124
93
  }
125
94
  export async function verifyEmail(userId, token) {
126
95
  _checkId(userId);
127
- await fetchAPI('POST', 'users/:id/verify_email', { token }, userId);
96
+ await fetchAPI('POST', 'users/:id/verify/email', { token }, userId);
128
97
  }
129
98
  export async function getPasskeys(userId) {
130
99
  _checkId(userId);
131
- const result = await fetchAPI('GET', 'users/:id/passkeys', {}, userId);
132
- for (const passkey of result) {
133
- passkey.createdAt = new Date(passkey.createdAt);
134
- }
135
- return result;
100
+ return await fetchAPI('GET', 'users/:id/passkeys', {}, userId);
136
101
  }
137
102
  /**
138
103
  * Create a new passkey for an existing user.
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import Dialog from './Dialog.svelte';
3
2
  import type { HTMLDialogAttributes } from 'svelte/elements';
4
3
 
5
4
  let {
@@ -59,7 +58,7 @@
59
58
  <button type="submit" class={['submit', submitDanger && 'danger']}>{submitText}</button>
60
59
  {/snippet}
61
60
 
62
- <Dialog bind:dialog {onclose} {...rest}>
61
+ <dialog bind:this={dialog} {onclose} {...rest}>
63
62
  {@render header?.()}
64
63
  <form {onsubmit} class="main" method="dialog">
65
64
  {#if error}
@@ -76,7 +75,7 @@
76
75
  {/if}
77
76
  </form>
78
77
  {@render footer?.()}
79
- </Dialog>
78
+ </dialog>
80
79
 
81
80
  <style>
82
81
  .actions {
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import ClipboardCopy from './ClipboardCopy.svelte';
3
+
4
+ const { url }: { url: string } = $props();
5
+
6
+ const href = $derived(new URL(url, location.origin).href);
7
+ </script>
8
+
9
+ <pre class="URLText"><span>{href}</span><ClipboardCopy value={href} --size="16px" /></pre>
10
+
11
+ <style>
12
+ .URLText {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+
17
+ span {
18
+ overflow-x: scroll;
19
+ }
20
+ }
21
+ </style>
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { ZodObject } from 'zod';
3
+ import ZodInput from './ZodInput.svelte';
4
+
5
+ interface Props {
6
+ rootValue: any;
7
+ schema: ZodObject;
8
+ labels: Record<string, string>;
9
+ updateValue(value: any): void;
10
+ idPrefix?: string;
11
+ }
12
+
13
+ let { rootValue = $bindable(), schema, labels, updateValue, idPrefix }: Props = $props();
14
+ </script>
15
+
16
+ <div class="ZodForm">
17
+ {#each Object.keys(schema.shape).sort((a, b) => a.localeCompare(b)) as path}
18
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {path} schema={schema.shape[path]} label={labels[path] || path} />
19
+ {/each}
20
+ </div>
21
+
22
+ <style>
23
+ .ZodForm {
24
+ display: flex;
25
+ flex-direction: column;
26
+ align-items: stretch;
27
+ gap: 1em;
28
+ }
29
+ </style>
@@ -0,0 +1,228 @@
1
+ <script lang="ts">
2
+ import type { ZodPref } from '@axium/core';
3
+ import type { HTMLInputAttributes } from 'svelte/elements';
4
+ import { getByString, pick, setByString } from 'utilium';
5
+ import Icon from './Icon.svelte';
6
+ import ZodInput from './ZodInput.svelte';
7
+ import { prettifyError } from 'zod';
8
+
9
+ interface Props {
10
+ idPrefix?: string;
11
+ rootValue: any;
12
+ path: string;
13
+ label?: string;
14
+ schema: ZodPref;
15
+ defaultValue?: any;
16
+ optional?: boolean;
17
+ updateValue(value: any): void;
18
+ }
19
+
20
+ let { rootValue = $bindable(), label, path, schema, optional = false, defaultValue, idPrefix, updateValue }: Props = $props();
21
+ const id = (idPrefix ? idPrefix + ':' : '') + path.replaceAll(' ', '_');
22
+
23
+ let input = $state<HTMLInputElement | HTMLSelectElement>()!;
24
+ let value = $state<any>(getByString(rootValue, path));
25
+
26
+ let error = $state();
27
+
28
+ function dateAttr(date: Date | null, format: 'date' | 'time' | 'datetime' | 'time+sec') {
29
+ if (!date) return null;
30
+
31
+ const pad = (n: number) => String(n).padStart(2, '0');
32
+
33
+ const yyyy = date.getFullYear();
34
+ const mm = pad(date.getMonth() + 1);
35
+ const dd = pad(date.getDate());
36
+ const dateStr = `${yyyy}-${mm}-${dd}`;
37
+
38
+ const HH = pad(date.getHours());
39
+ const MM = pad(date.getMinutes());
40
+ const timeStr = `${HH}:${MM}`;
41
+
42
+ switch (format) {
43
+ case 'date':
44
+ return dateStr;
45
+ case 'time':
46
+ return timeStr;
47
+ case 'time+sec':
48
+ return `${timeStr}:${pad(date.getSeconds())}`;
49
+ case 'datetime':
50
+ default:
51
+ return `${dateStr}T${timeStr}`;
52
+ }
53
+ }
54
+
55
+ function onchange(e: Event) {
56
+ let val;
57
+
58
+ try {
59
+ val = schema.parse(value);
60
+ error = null;
61
+ } catch (e: any) {
62
+ error = prettifyError(e);
63
+ return;
64
+ }
65
+
66
+ if (e instanceof KeyboardEvent && e.key !== 'Enter') return;
67
+
68
+ const oldValue = getByString(rootValue, path);
69
+ if (val == oldValue) return;
70
+
71
+ if (defaultValue == val) {
72
+ const parts = path.split('.');
73
+ const prop = parts.pop()!;
74
+ delete getByString<Record<string, any>>(rootValue, parts.join('.'))[prop];
75
+ } else setByString(rootValue, path, val);
76
+
77
+ updateValue(rootValue);
78
+ }
79
+
80
+ const onkeyup = onchange;
81
+ </script>
82
+
83
+ {#snippet _in(rest: HTMLInputAttributes)}
84
+ <div class="ZodInput">
85
+ <label for={id}>{label || path}</label>
86
+ {#if error}<span class="ZodInput-error error-text">{error}</span>{/if}
87
+ <input
88
+ bind:this={input}
89
+ {id}
90
+ {...rest}
91
+ bind:value
92
+ {onchange}
93
+ {onkeyup}
94
+ required={!optional}
95
+ {defaultValue}
96
+ class={[error && 'error']}
97
+ />
98
+ </div>
99
+ {/snippet}
100
+
101
+ {#if schema.type == 'string'}
102
+ {@render _in({ type: schema.format == 'email' ? 'email' : 'text', ...pick(schema, 'minLength', 'maxLength') })}
103
+ {:else if schema.type == 'number'}
104
+ {@render _in({ type: 'number', min: schema.minValue, max: schema.maxValue, step: schema.format?.includes('int') ? 1 : 0.1 })}
105
+ {:else if schema.type == 'bigint'}
106
+ {@render _in({ type: 'number', min: Number(schema.minValue), max: Number(schema.maxValue), step: 1 })}
107
+ {:else if schema.type == 'boolean'}
108
+ <div class="ZodInput">
109
+ <label for="{id}:checkbox">{label || path}</label>
110
+ <input bind:checked={value} bind:this={input} id="{id}:checkbox" type="checkbox" {onchange} {onkeyup} required={!optional} />
111
+ <label for="{id}:checkbox" {id} class="checkbox">
112
+ {#if value}<Icon i="check" --size="1.3em" />{/if}
113
+ </label>
114
+ </div>
115
+ {:else if schema.type == 'date'}
116
+ {@render _in({
117
+ type: 'date',
118
+ min: dateAttr(schema.minDate, 'date'),
119
+ max: dateAttr(schema.maxDate, 'date'),
120
+ })}
121
+ {:else if schema.type == 'file'}
122
+ <!-- todo -->
123
+ {:else if schema.type == 'literal'}
124
+ <div class="ZodInput">
125
+ <label for={id}>{label || path}</label>
126
+ <select bind:this={input} bind:value {id} {onchange} {onkeyup} required={!optional}>
127
+ {#each schema.values as value}
128
+ <option {value} selected={value === value}>{value}</option>
129
+ {/each}
130
+ </select>
131
+ </div>
132
+ {:else if schema.type == 'template_literal'}
133
+ <!-- todo -->
134
+ {:else if schema.type == 'default'}
135
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {path} schema={schema.def.innerType} defaultValue={schema.def.defaultValue} />
136
+ {:else if schema.type == 'nullable' || schema.type == 'optional'}
137
+ <!-- defaults are handled differently -->
138
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {path} {defaultValue} schema={schema.def.innerType} optional={true} />
139
+ {:else if schema.type == 'array'}
140
+ <div class="ZodInput">
141
+ <label for={id}>{label || path}</label>
142
+ {#each value, i}
143
+ <div class="ZodInput-element">
144
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {defaultValue} path="{path}.{i}" schema={schema.element} />
145
+ </div>
146
+ {:else}
147
+ <i>Empty</i>
148
+ {/each}
149
+ </div>
150
+ {:else if schema.type == 'record'}
151
+ <div class="ZodInput-record">
152
+ {#each Object.keys(value) as key}
153
+ <div class="ZodInput-record-entry">
154
+ <label for={id}>{key}</label>
155
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {defaultValue} path="{path}.{key}" schema={schema.valueType} />
156
+ </div>
157
+ {/each}
158
+ </div>
159
+ {:else if schema.type == 'object'}
160
+ <!-- <div class="ZodInput-object"> -->
161
+ {#each Object.entries(schema.shape) as [key, value]}
162
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {defaultValue} path="{path}.{key}" schema={value} />
163
+ {/each}
164
+ <!-- </div> -->
165
+ {:else if schema.type == 'tuple'}
166
+ <div class="ZodInput-tuple" data-rest={schema.def.rest}>
167
+ {#each schema.def.items as item, i}
168
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {defaultValue} path="{path}.{i}" schema={item} />
169
+ {/each}
170
+ </div>
171
+ {:else if schema.type == 'enum'}
172
+ <div class="ZodInput">
173
+ <label for={id}>{label || path}</label>
174
+ <select bind:this={input} {id} {onchange} {onkeyup} bind:value required={!optional}>
175
+ {#each Object.entries(schema.enum) as [key, value]}
176
+ <option {value} selected={value === value}>{key}</option>
177
+ {/each}
178
+ </select>
179
+ </div>
180
+ {:else}
181
+ <!-- No idea how to render this -->
182
+ <i class="error-text">Invalid preference type: {JSON.stringify((schema as ZodPref)?.def?.type)}</i>
183
+ {/if}
184
+
185
+ <style>
186
+ input[type='checkbox'] {
187
+ display: none;
188
+ }
189
+
190
+ label.checkbox {
191
+ cursor: pointer;
192
+ width: 1.5em;
193
+ height: 1.5em;
194
+ border: 1px solid var(--border-accent);
195
+ border-radius: 0.5em;
196
+ display: inline-flex;
197
+ justify-content: center;
198
+ align-items: center;
199
+ }
200
+
201
+ .ZodInput-error {
202
+ position: fixed;
203
+ position-anchor: --zod-input;
204
+ bottom: calc(anchor(top) - 0.3em);
205
+ left: anchor(left);
206
+ }
207
+
208
+ .ZodInput {
209
+ display: grid;
210
+ grid-template-columns: 1fr 1fr;
211
+ gap: 1em;
212
+ anchor-scope: --zod-input;
213
+
214
+ input {
215
+ anchor-name: --zod-input;
216
+ }
217
+ }
218
+
219
+ .ZodInput-object,
220
+ .ZodInput-record,
221
+ .ZodInput-array,
222
+ .ZodInput-tuple {
223
+ display: flex;
224
+ flex-direction: column;
225
+ align-items: stretch;
226
+ gap: 0.25em;
227
+ }
228
+ </style>
package/lib/index.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  export { default as AccessControlDialog } from './AccessControlDialog.svelte';
2
2
  export { default as AppMenu } from './AppMenu.svelte';
3
3
  export { default as ClipboardCopy } from './ClipboardCopy.svelte';
4
- export { default as Dialog } from './Dialog.svelte';
5
4
  export { default as FormDialog } from './FormDialog.svelte';
6
5
  export { default as Icon } from './Icon.svelte';
7
6
  export { default as Login } from './Login.svelte';
8
7
  export { default as Logout } from './Logout.svelte';
9
8
  export { default as NumberBar } from './NumberBar.svelte';
10
9
  export { default as Popover } from './Popover.svelte';
11
- export { default as Preference } from './Preference.svelte';
12
- export { default as Preferences } from './Preferences.svelte';
13
10
  export { default as Register } from './Register.svelte';
14
11
  export { default as SessionList } from './SessionList.svelte';
15
12
  export { default as Toast } from './Toast.svelte';
16
13
  export { default as Upload } from './Upload.svelte';
14
+ export { default as URLText } from './URLText.svelte';
17
15
  export { default as UserCard } from './UserCard.svelte';
18
16
  export { default as UserMenu } from './UserMenu.svelte';
19
17
  export { default as Version } from './Version.svelte';
20
18
  export { default as WithContextMenu } from './WithContextMenu.svelte';
19
+ export { default as ZodForm } from './ZodForm.svelte';
20
+ export { default as ZodInput } from './ZodInput.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
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.16.0",
43
+ "@axium/core": ">=0.18.0",
44
44
  "utilium": "^2.3.8",
45
45
  "zod": "^4.0.5",
46
46
  "svelte": "^5.36.0"
package/lib/Dialog.svelte DELETED
@@ -1,53 +0,0 @@
1
- <script lang="ts">
2
- import type { HTMLDialogAttributes } from 'svelte/elements';
3
-
4
- let { children, dialog = $bindable(), ...rest }: { children(): any; dialog?: HTMLDialogElement } & HTMLDialogAttributes = $props();
5
- </script>
6
-
7
- <dialog bind:this={dialog} {...rest}>
8
- {@render children()}
9
- </dialog>
10
-
11
- <!-- svelte-ignore css_unused_selector -->
12
- <style>
13
- dialog {
14
- border-radius: 1em;
15
- background: var(--bg-menu);
16
- border: 1px solid #8888;
17
- padding: 1em;
18
-
19
- form {
20
- display: contents;
21
- }
22
- }
23
-
24
- dialog::backdrop {
25
- background: #0003;
26
- }
27
-
28
- dialog[open] {
29
- animation: zoom 0.25s cubic-bezier(0.35, 1.55, 0.65, 1);
30
- }
31
-
32
- @keyframes zoom {
33
- from {
34
- transform: scale(0.95);
35
- }
36
- to {
37
- transform: scale(1);
38
- }
39
- }
40
-
41
- dialog[open]::backdrop {
42
- animation: fade 0.25s ease-out;
43
- }
44
-
45
- @keyframes fade {
46
- from {
47
- opacity: 0;
48
- }
49
- to {
50
- opacity: 1;
51
- }
52
- }
53
- </style>
@@ -1,159 +0,0 @@
1
- <script lang="ts">
2
- import { fetchAPI } from '@axium/client/requests';
3
- import type { Preferences, ZodPref } from '@axium/core';
4
- import type { HTMLInputAttributes } from 'svelte/elements';
5
- import { getByString, pick, setByString } from 'utilium';
6
- import Icon from './Icon.svelte';
7
- import Preference from './Preference.svelte';
8
-
9
- interface Props {
10
- userId: string;
11
- preferences: Preferences;
12
- path: string;
13
- schema: ZodPref;
14
- defaultValue?: any;
15
- optional?: boolean;
16
- }
17
-
18
- let { preferences = $bindable(), userId, path, schema, optional = false, defaultValue }: Props = $props();
19
- const id = $props.id();
20
-
21
- let input = $state<HTMLInputElement | HTMLSelectElement>()!;
22
- let checked = $state(schema.def.type == 'boolean' && getByString<boolean>(preferences, path));
23
- const initialValue = $derived<any>(getByString(preferences, path));
24
-
25
- function dateAttr(date: Date | null, format: 'date' | 'time' | 'datetime' | 'time+sec') {
26
- if (!date) return null;
27
-
28
- const pad = (n: number) => String(n).padStart(2, '0');
29
-
30
- const yyyy = date.getFullYear();
31
- const mm = pad(date.getMonth() + 1);
32
- const dd = pad(date.getDate());
33
- const dateStr = `${yyyy}-${mm}-${dd}`;
34
-
35
- const HH = pad(date.getHours());
36
- const MM = pad(date.getMinutes());
37
- const timeStr = `${HH}:${MM}`;
38
-
39
- switch (format) {
40
- case 'date':
41
- return dateStr;
42
- case 'time':
43
- return timeStr;
44
- case 'time+sec':
45
- return `${timeStr}:${pad(date.getSeconds())}`;
46
- case 'datetime':
47
- default:
48
- return `${dateStr}T${timeStr}`;
49
- }
50
- }
51
-
52
- function onchange(e: Event) {
53
- const value = schema.parse(input instanceof HTMLInputElement && input.type === 'checkbox' ? input.checked : input.value);
54
- const oldValue = getByString(preferences, path);
55
- if (value == oldValue) return;
56
-
57
- if (defaultValue == value) {
58
- const parts = path.split('.');
59
- const prop = parts.pop()!;
60
- delete getByString<Record<string, any>>(preferences, parts.join('.'))[prop];
61
- } else setByString(preferences, path, value);
62
-
63
- fetchAPI('PATCH', 'users/:id', { preferences }, userId);
64
- }
65
- </script>
66
-
67
- {#snippet _in(rest: HTMLInputAttributes)}
68
- <input bind:this={input} {id} {...rest} value={initialValue} {onchange} required={!optional} {defaultValue} />
69
- {/snippet}
70
-
71
- {#if schema.type == 'string'}
72
- {@render _in({ type: schema.format == 'email' ? 'email' : 'text', ...pick(schema, 'minLength', 'maxLength') })}
73
- {:else if schema.type == 'number'}
74
- {@render _in({ type: 'number', min: schema.minValue, max: schema.maxValue, step: schema.format?.includes('int') ? 1 : 0.1 })}
75
- {:else if schema.type == 'bigint'}
76
- {@render _in({ type: 'number', min: Number(schema.minValue), max: Number(schema.maxValue), step: 1 })}
77
- {:else if schema.type == 'boolean'}
78
- <input bind:checked bind:this={input} {id} type="checkbox" {onchange} required={!optional} />
79
- <label for={id} class="checkbox">
80
- {#if checked}<Icon i="check" --size="1.3em" />{/if}
81
- </label>
82
- {:else if schema.type == 'date'}
83
- {@render _in({
84
- type: 'date',
85
- min: dateAttr(schema.minDate, 'date'),
86
- max: dateAttr(schema.maxDate, 'date'),
87
- })}
88
- {:else if schema.type == 'file'}
89
- <!-- todo -->
90
- {:else if schema.type == 'literal'}
91
- <select bind:this={input} {id} {onchange} required={!optional}>
92
- {#each schema.values as value}
93
- <option {value} selected={initialValue === value}>{value}</option>
94
- {/each}
95
- </select>
96
- {:else if schema.type == 'template_literal'}
97
- <!-- todo -->
98
- {:else if schema.type == 'default'}
99
- <Preference {userId} bind:preferences {path} schema={schema.def.innerType} defaultValue={schema.def.defaultValue} />
100
- {:else if schema.type == 'nullable' || schema.type == 'optional'}
101
- <!-- defaults are handled differently -->
102
- <Preference {userId} bind:preferences {path} {defaultValue} schema={schema.def.innerType} optional={true} />
103
- {:else if schema.type == 'array'}
104
- <div class="pref-sub">
105
- {#each initialValue, i}
106
- <div class="pref-record-entry">
107
- <Preference {userId} bind:preferences {defaultValue} path="{path}.{i}" schema={schema.element} />
108
- </div>
109
- {/each}
110
- </div>
111
- {:else if schema.type == 'record'}
112
- <div class="pref-sub">
113
- {#each Object.keys(initialValue) as key}
114
- <div class="pref-record-entry">
115
- <label for={id}>{key}</label>
116
- <Preference {userId} bind:preferences {defaultValue} path="{path}.{key}" schema={schema.valueType} />
117
- </div>
118
- {/each}
119
- </div>
120
- {:else if schema.type == 'object'}
121
- {#each Object.entries(schema.shape) as [key, value]}
122
- <div class="pref-sub">
123
- <label for={id}>{key}</label>
124
- <Preference {userId} bind:preferences {defaultValue} path="{path}.{key}" schema={value} />
125
- </div>
126
- {/each}
127
- {:else if schema.type == 'tuple'}
128
- <div class="pref-sub" data-rest={schema.def.rest}>
129
- {#each schema.def.items as item, i}
130
- <Preference {userId} bind:preferences {defaultValue} path="{path}.{i}" schema={item} />
131
- {/each}
132
- </div>
133
- {:else if schema.type == 'enum'}
134
- <select bind:this={input} {id} {onchange} required={!optional}>
135
- {#each Object.entries(schema.enum) as [key, value]}
136
- <option {value} selected={initialValue === value}>{key}</option>
137
- {/each}
138
- </select>
139
- {:else}
140
- <!-- No idea how to render this -->
141
- <i class="error-text">Invalid preference type: {JSON.stringify((schema as ZodPref)?.def?.type)}</i>
142
- {/if}
143
-
144
- <style>
145
- input[type='checkbox'] {
146
- display: none;
147
- }
148
-
149
- label.checkbox {
150
- cursor: pointer;
151
- width: 1.5em;
152
- height: 1.5em;
153
- border: 1px solid var(--border-accent);
154
- border-radius: 0.5em;
155
- display: inline-flex;
156
- justify-content: center;
157
- align-items: center;
158
- }
159
- </style>
@@ -1,27 +0,0 @@
1
- <script lang="ts">
2
- import { preferenceLabels, Preferences } from '@axium/core';
3
- import Preference from './Preference.svelte';
4
-
5
- interface Props {
6
- userId: string;
7
- preferences: Preferences;
8
- }
9
-
10
- let { preferences = $bindable(), userId }: Props = $props();
11
- const id = $props.id();
12
- </script>
13
-
14
- {#each Object.keys(Preferences.shape) as (keyof Preferences)[] as path}
15
- <div class="pref">
16
- <label for={id}>{preferenceLabels[path]}</label>
17
- <Preference {userId} bind:preferences {path} schema={Preferences.shape[path]} />
18
- </div>
19
- {/each}
20
-
21
- <style>
22
- .pref {
23
- display: grid;
24
- grid-template-columns: 1fr 1fr;
25
- gap: 1em;
26
- }
27
- </style>