@axium/client 0.20.2 → 0.22.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.
@@ -19,7 +19,6 @@ export declare function session(): {
19
19
  isAdmin: boolean;
20
20
  isSuspended: boolean;
21
21
  emailVerified?: Date | null | undefined;
22
- image?: string | null | undefined;
23
22
  };
24
23
  name?: string | null | undefined;
25
24
  };
package/dist/config.d.ts CHANGED
@@ -16,7 +16,6 @@ export declare const ClientConfig: z.ZodObject<{
16
16
  name: z.ZodString;
17
17
  email: z.ZodEmail;
18
18
  emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
19
- image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
20
19
  preferences: z.ZodLazy<z.ZodObject<{
21
20
  debug: z.ZodDefault<z.ZodBoolean>;
22
21
  }, z.core.$strip>>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './access.js';
2
+ export * from './cache.js';
2
3
  export * from './config.js';
3
4
  export * from './locales.js';
4
5
  export * from './requests.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './access.js';
2
+ export * from './cache.js';
2
3
  export * from './config.js';
3
4
  export * from './locales.js';
4
5
  export * from './requests.js';
package/dist/locales.d.ts CHANGED
@@ -112,8 +112,13 @@ declare let currentLoaded: {
112
112
  readonly title: "Passkeys";
113
113
  };
114
114
  readonly personal_info: "Personal Information";
115
+ readonly pfp: {
116
+ readonly update: "Upload";
117
+ readonly remove: "Remove";
118
+ readonly toast_removed: "Profile picture removed";
119
+ readonly toast_updated: "Profile picture updated";
120
+ };
115
121
  readonly preferences: "Preferences";
116
- readonly profile_alt: "User profile";
117
122
  readonly sessions: "Sessions";
118
123
  readonly title: "Your Account";
119
124
  readonly user_id: "User ID";
@@ -230,10 +235,19 @@ declare let currentLoaded: {
230
235
  readonly failed: "Login Failed";
231
236
  };
232
237
  };
238
+ readonly location: {
239
+ readonly country: "Country";
240
+ readonly subdivision: "State / province";
241
+ readonly locality: "City / town";
242
+ readonly postal_code: "Postal code";
243
+ readonly street1: "Street address";
244
+ readonly street2: "Street address line 2";
245
+ };
233
246
  readonly preference: {
234
247
  readonly debug: "Debug mode";
235
248
  };
236
249
  };
250
+ export declare let currentMonthNames: string[];
237
251
  /**
238
252
  * Current locale
239
253
  */
@@ -254,6 +268,8 @@ type _ArgsValue<V extends string[]> = UnionToIntersection<{
254
268
  type Replacements<K extends string> = ReplacementOptions & (GetByString<Locale, K> extends string ? _ArgsValue<Split<GetByString<Locale, K> & string, '{'>> : Record<string, any>);
255
269
  type ReplacementsArgs<K extends string> = {} extends Replacements<K> ? [replacements?: Replacements<K>] : [replacements: Replacements<K>];
256
270
  export declare function useLocale(newLocale: string): void;
271
+ export declare function countryName(code: string): string | undefined;
272
+ export declare function dateField(name: string): string | undefined;
257
273
  export declare function escape(text: string): string;
258
274
  /**
259
275
  * Get localized text for a given translation key
package/dist/locales.js CHANGED
@@ -15,7 +15,8 @@ export function extendLocale(locale, data) {
15
15
  debug('Extending locale: ' + locale);
16
16
  deepAssign(loadedLocales[locale], data);
17
17
  }
18
- let currentLoaded = en;
18
+ let currentLoaded = en, currentRegionNames, currentDateFields;
19
+ export let currentMonthNames;
19
20
  /**
20
21
  * Current locale
21
22
  */
@@ -25,6 +26,17 @@ export function useLocale(newLocale) {
25
26
  throw new Error('Locale is not available: ' + newLocale);
26
27
  currentLocale = newLocale;
27
28
  currentLoaded = loadedLocales[newLocale];
29
+ currentRegionNames = new Intl.DisplayNames(newLocale, { type: 'region' });
30
+ currentDateFields = new Intl.DisplayNames(newLocale, { type: 'dateTimeField' });
31
+ const formatter = new Intl.DateTimeFormat(newLocale, { month: 'long' });
32
+ currentMonthNames = Array.from({ length: 12 }, (_, monthIndex) => formatter.format(new Date(Date.UTC(2000, monthIndex + 1, 1))));
33
+ }
34
+ useLocale('en');
35
+ export function countryName(code) {
36
+ return currentRegionNames.of(code);
37
+ }
38
+ export function dateField(name) {
39
+ return currentDateFields.of(name);
28
40
  }
29
41
  const localeReplacement = /\{(\w+)\}/g;
30
42
  const escapePattern = /[&<>"']/g;
package/lib/Icon.svelte CHANGED
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- const { i, ...rest } = $props();
2
+ import type { SVGAttributes } from 'svelte/elements';
3
+ const { i, ...rest }: { i: string } & SVGAttributes<SVGSVGElement> = $props();
3
4
  const [style, id] = $derived(i.includes('/') ? i.split('/') : ['solid', i]);
4
5
  const href = $derived(`/icons/${style}.svg#${id}`);
5
6
  </script>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { countryName, text } from '@axium/client';
3
+ import { countries, type LocationInit } from '@axium/core';
4
+
5
+ let { value = $bindable<LocationInit>() }: { value: LocationInit } = $props();
6
+ </script>
7
+
8
+ <select bind:value={value.country} placeholder={text('location.country')}>
9
+ {#each countries as country}
10
+ <option value={country}>{countryName(country)}</option>
11
+ {/each}
12
+ </select>
13
+
14
+ <input type="text" placeholder={text('location.street1')} bind:value={value.street1} />
15
+ <input type="text" placeholder={text('location.street2')} bind:value={value.street2} />
16
+ <input type="text" placeholder={text('location.locality')} bind:value={value.locality} />
17
+ <div>
18
+ <input type="text" placeholder={text('location.subdivision')} bind:value={value.subdivision} />
19
+ <input type="text" placeholder={text('location.postal_code')} bind:value={value.postalCode} />
20
+ </div>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { text } from '@axium/client';
3
- import { getUserImage } from '@axium/core';
4
- import type { User } from '@axium/core/user';
3
+ import type { UserPublic } from '@axium/core/user';
4
+ import UserPFP from './UserPFP.svelte';
5
5
 
6
6
  const {
7
7
  user,
@@ -10,7 +10,7 @@
10
10
  href,
11
11
  you = false,
12
12
  }: {
13
- user: Partial<User>;
13
+ user: UserPublic;
14
14
  /** If true, don't show the picture */
15
15
  compact?: boolean;
16
16
  /** Whether the user is viewing their own profile */
@@ -24,7 +24,7 @@
24
24
 
25
25
  <a class={['User', self && 'self']} {href}>
26
26
  {#if !compact}
27
- <img src={getUserImage(user)} alt={user.name} />
27
+ <UserPFP {user} />
28
28
  {/if}
29
29
  {user.name}
30
30
  {#if self && you}
@@ -38,12 +38,4 @@
38
38
  width: max-content;
39
39
  height: max-content;
40
40
  }
41
-
42
- img {
43
- width: 2em;
44
- height: 2em;
45
- border-radius: 50%;
46
- vertical-align: middle;
47
- margin-right: 0.5em;
48
- }
49
41
  </style>
@@ -1,9 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { text } from '@axium/client';
3
3
  import { fetchAPI } from '@axium/client/requests';
4
- import { getUserImage, type UserPublic } from '@axium/core';
4
+ import type { UserPublic } from '@axium/core';
5
5
  import { errorText } from 'ioium';
6
6
  import Icon from './Icon.svelte';
7
+ import UserPFP from './UserPFP.svelte';
7
8
 
8
9
  const {
9
10
  onSelect,
@@ -63,7 +64,7 @@
63
64
  {#if !excludeTargets.includes(result.target)}
64
65
  <div class="result" onclick={select(result.target)}>
65
66
  {#if result.type == 'user'}
66
- <span><img src={getUserImage(result.value)} alt={result.value.name} />{result.value.name}</span>
67
+ <span><UserPFP user={result.value} /> {result.value.name}</span>
67
68
  {:else if result.type == 'role'}
68
69
  <span>
69
70
  <span class="icon-text non-user"><Icon i="at" />{result.value}</span>
@@ -135,12 +136,4 @@
135
136
  cursor: pointer;
136
137
  background-color: var(--bg-strong);
137
138
  }
138
-
139
- img {
140
- width: 2em;
141
- height: 2em;
142
- border-radius: 50%;
143
- vertical-align: middle;
144
- margin-right: 0.5em;
145
- }
146
139
  </style>
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { fetchAPI, text } from '@axium/client';
3
- import { getUserImage } from '@axium/core';
4
3
  import type { UserPublic } from '@axium/core/user';
5
4
  import Icon from './Icon.svelte';
6
5
  import Logout from './Logout.svelte';
7
6
  import Popover from './Popover.svelte';
7
+ import UserPFP from './UserPFP.svelte';
8
8
 
9
9
  const { user }: { user?: UserPublic } = $props();
10
10
  </script>
@@ -13,7 +13,7 @@
13
13
  <Popover>
14
14
  {#snippet toggle()}
15
15
  <div class="UserMenu toggle">
16
- <img src={getUserImage(user)} alt={user.name} />
16
+ <UserPFP {user} />
17
17
  {user.name}
18
18
  </div>
19
19
  {/snippet}
@@ -73,14 +73,6 @@
73
73
  background-color: var(--bg-alt);
74
74
  }
75
75
 
76
- img {
77
- width: 2em;
78
- height: 2em;
79
- border-radius: 50%;
80
- vertical-align: middle;
81
- margin-right: 0.5em;
82
- }
83
-
84
76
  .logout {
85
77
  color: hsl(0 33 var(--fg-light));
86
78
  font-size: 16px;
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import type { UserPublic } from '@axium/core';
3
+ import { colorHashRGB } from '@axium/core/color';
4
+
5
+ let { user, isDefault = $bindable() }: { user: UserPublic; isDefault?: boolean } = $props();
6
+
7
+ const defaultImage = $derived(
8
+ `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" style="background-color:${colorHashRGB(user.name ?? '\0')};display:flex;align-items:center;justify-content:center;">
9
+ <text x="23" y="28" style="font-family:sans-serif;font-weight:bold;" fill="white">${(user.name.replaceAll(/\W/g, '') || '?')[0]}</text>
10
+ </svg>`.replaceAll(/[\t\n]/g, '')
11
+ );
12
+
13
+ let src = $state(`/raw/pfp/${user.id}`);
14
+
15
+ $effect(() => {
16
+ // Reset the attempted source when the user changes
17
+ src = `/raw/pfp/${user.id}`;
18
+ });
19
+ </script>
20
+
21
+ <img
22
+ class="UserPFP"
23
+ {src}
24
+ alt={user.name}
25
+ onerror={() => {
26
+ isDefault = true;
27
+ src = defaultImage;
28
+ }}
29
+ />
30
+
31
+ <style>
32
+ img.UserPFP {
33
+ width: var(--size, 2em);
34
+ height: var(--size, 2em);
35
+ border-radius: 50%;
36
+ border: 1px solid #8888;
37
+ vertical-align: middle;
38
+ margin-right: 0.5em;
39
+ /* see https://drafts.csswg.org/css-image-animation-1/ */
40
+ image-animation: stopped;
41
+ }
42
+ </style>
@@ -5,7 +5,7 @@
5
5
  interface Props {
6
6
  rootValue: any;
7
7
  schema: ZodObject;
8
- labels: Record<string, string>;
8
+ labels?: Record<string, string>;
9
9
  updateValue(value: any): void;
10
10
  idPrefix?: string;
11
11
  }
@@ -15,7 +15,7 @@
15
15
 
16
16
  <div class="ZodForm">
17
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} />
18
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {path} schema={schema.shape[path]} label={labels?.[path] || path} />
19
19
  {/each}
20
20
  </div>
21
21
 
@@ -14,15 +14,17 @@
14
14
  path: string;
15
15
  label?: string;
16
16
  schema: ZodPref;
17
+ _parseSchema?: ZodPref;
17
18
  defaultValue?: any;
18
19
  optional?: boolean;
19
- noLabel?: boolean;
20
+ readonly?: boolean;
21
+ noLabel?: boolean | 'placeholder';
20
22
  updateValue(value: any): void;
21
23
  }
22
24
 
23
25
  let { rootValue = $bindable(), ...props }: Props = $props();
24
26
 
25
- let { label, path, schema, optional, defaultValue, idPrefix, updateValue, noLabel } = props;
27
+ let { label, path, schema, _parseSchema = schema, optional, readonly, defaultValue, idPrefix, updateValue, noLabel } = props;
26
28
 
27
29
  const id = (idPrefix ? idPrefix + ':' : '') + path.replaceAll(' ', '_');
28
30
 
@@ -85,13 +87,28 @@
85
87
  const oninput = onchange;
86
88
 
87
89
  const labelText = $derived(zKeys.has(props.schema) ? text(zKeys.get(props.schema)!.key) : label || path);
90
+ const placeholder = noLabel == 'placeholder' ? labelText : undefined;
91
+
92
+ /** Array element types that indicate the array should put elements on their own lines */
93
+ const largeArrayTypes = ['object', 'array', 'record', 'tuple'];
88
94
  </script>
89
95
 
90
96
  {#snippet _in(rest: HTMLInputAttributes)}
91
97
  <div class="ZodInput">
92
98
  {#if !noLabel}<label for={id}>{labelText}</label>{/if}
93
99
  {#if error}<span class="ZodInput-error">{error}</span>{/if}
94
- <input {id} {...rest} bind:value {onchange} {oninput} required={!optional} {defaultValue} class={[error && 'error']} />
100
+ <input
101
+ {id}
102
+ {...rest}
103
+ bind:value
104
+ {onchange}
105
+ {oninput}
106
+ required={!optional}
107
+ {defaultValue}
108
+ {placeholder}
109
+ {readonly}
110
+ class={[error && 'error']}
111
+ />
95
112
  </div>
96
113
  {/snippet}
97
114
 
@@ -104,7 +121,7 @@
104
121
  {:else if schema.type == 'boolean'}
105
122
  <div class="ZodInput">
106
123
  {#if !noLabel}<label for="{id}:checkbox">{labelText}</label>{/if}
107
- <input bind:checked={value} id="{id}:checkbox" type="checkbox" {onchange} required={!optional} />
124
+ <input bind:checked={value} id="{id}:checkbox" type="checkbox" {onchange} required={!optional} {placeholder} />
108
125
  <label for="{id}:checkbox" {id} class="checkbox">
109
126
  {#if value}<Icon i="check" --size="1.3em" />{/if}
110
127
  </label>
@@ -128,19 +145,22 @@
128
145
  </div>
129
146
  {:else if schema.type == 'template_literal'}
130
147
  <!-- todo -->
148
+ {:else if schema.type == 'readonly'}
149
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.innerType} readonly />
131
150
  {:else if schema.type == 'default'}
132
151
  <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.innerType} defaultValue={schema.def.defaultValue} />
133
152
  {:else if schema.type == 'nullable' || schema.type == 'optional'}
134
153
  <!-- defaults are handled differently -->
135
154
  <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.innerType} optional={true} />
155
+ {:else if schema.type == 'nonoptional'}
156
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.innerType} optional={false} />
136
157
  {:else if schema.type == 'array'}
137
158
  <div class="ZodInput">
138
159
  {#if !noLabel}<label for={id}>{labelText}</label>{/if}
139
- {#if error}<span class="ZodInput-error">{error}</span>{/if}
140
- <div class="ZodInput-array">
160
+ <div class={['ZodInput-array', largeArrayTypes.includes(schema.element.type) && 'large']}>
141
161
  {#each value, i}
142
162
  <div class="ZodInput-element">
143
- <input id="{id}.{i}" bind:value={value[i]} {oninput} required={!optional} {defaultValue} class={[error && 'error']} />
163
+ <ZodInput bind:rootValue {updateValue} {idPrefix} path="{path}.{i}" schema={schema.element} noLabel={noLabel || true} />
144
164
  <button
145
165
  onclick={e => {
146
166
  value.splice(i, 1);
@@ -151,8 +171,11 @@
151
171
  </button>
152
172
  </div>
153
173
  {/each}
154
- <button onclick={() => value.push('')}>
174
+ <button class="icon-text" onclick={() => value.push('')}>
155
175
  <Icon i="plus" --size="16px" />
176
+ {#if noLabel == 'placeholder'}
177
+ <span>{labelText}</span>
178
+ {/if}
156
179
  </button>
157
180
  </div>
158
181
  </div>
@@ -161,14 +184,14 @@
161
184
  {#each Object.keys(value) as key}
162
185
  <div class="ZodInput-record-entry">
163
186
  {#if !noLabel}<label for={id}>{key}</label>{/if}
164
- <ZodInput bind:rootValue {updateValue} {idPrefix} path="{path}.{key}" schema={schema.valueType} />
187
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {noLabel} path="{path}.{key}" schema={schema.valueType} />
165
188
  </div>
166
189
  {/each}
167
190
  </div>
168
191
  {:else if schema.type == 'object'}
169
192
  <!-- <div class="ZodInput-object"> -->
170
193
  {#each Object.entries(schema.shape) as [key, value]}
171
- <ZodInput bind:rootValue {updateValue} {idPrefix} path="{path}.{key}" schema={value} />
194
+ <ZodInput bind:rootValue {updateValue} {idPrefix} {noLabel} path="{path}.{key}" schema={value} />
172
195
  {/each}
173
196
  <!-- </div> -->
174
197
  {:else if schema.type == 'tuple'}
@@ -187,6 +210,14 @@
187
210
  {/each}
188
211
  </select>
189
212
  </div>
213
+ {:else if schema.type == 'success'}
214
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.innerType} {_parseSchema} />
215
+ {:else if schema.type == 'pipe'}
216
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.in} {_parseSchema} />
217
+ {:else if schema.type == 'union'}
218
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.options[0]} {_parseSchema} />
219
+ {:else if schema.type == 'intersection'}
220
+ <ZodInput bind:rootValue {...props} label={labelText} schema={schema.def.left} {_parseSchema} />
190
221
  {:else}
191
222
  <!-- No idea how to render this -->
192
223
  <i class="error">{text('ZodInput.invalid_type', { type: JSON.stringify((schema as ZodPref)?.def?.type) })}</i>
@@ -217,9 +248,14 @@
217
248
  gap: 0.5em;
218
249
  align-items: center;
219
250
 
220
- button {
221
- position: relative;
222
- right: 1em;
251
+ & > button {
252
+ display: contents;
253
+ }
254
+
255
+ & > .ZodInput {
256
+ gap: 0.25em;
257
+ display: flex;
258
+ flex-direction: column;
223
259
  }
224
260
  }
225
261
 
@@ -227,6 +263,10 @@
227
263
  display: flex;
228
264
  gap: 0.5em;
229
265
  align-items: center;
266
+
267
+ &.large {
268
+ flex-direction: column;
269
+ }
230
270
  }
231
271
 
232
272
  .ZodInput-object,
package/lib/index.ts CHANGED
@@ -4,6 +4,7 @@ export { default as ColorPicker } from './ColorPicker.svelte';
4
4
  export { default as ClipboardCopy } from './ClipboardCopy.svelte';
5
5
  export { default as FormDialog } from './FormDialog.svelte';
6
6
  export { default as Icon } from './Icon.svelte';
7
+ export { default as LocationSelect } from './LocationSelect.svelte';
7
8
  export { default as Login } from './Login.svelte';
8
9
  export { default as Logout } from './Logout.svelte';
9
10
  export { default as NumberBar } from './NumberBar.svelte';
@@ -15,6 +16,7 @@ export { default as Upload } from './Upload.svelte';
15
16
  export { default as URLText } from './URLText.svelte';
16
17
  export { default as UserCard } from './UserCard.svelte';
17
18
  export { default as UserDiscovery } from './UserDiscovery.svelte';
19
+ export { default as UserPFP } from './UserPFP.svelte';
18
20
  export { default as UserMenu } from './UserMenu.svelte';
19
21
  export { default as Version } from './Version.svelte';
20
22
  export { default as ZodForm } from './ZodForm.svelte';
package/locales/en.json CHANGED
@@ -106,8 +106,13 @@
106
106
  "title": "Passkeys"
107
107
  },
108
108
  "personal_info": "Personal Information",
109
+ "pfp": {
110
+ "update": "Upload",
111
+ "remove": "Remove",
112
+ "toast_removed": "Profile picture removed",
113
+ "toast_updated": "Profile picture updated"
114
+ },
109
115
  "preferences": "Preferences",
110
- "profile_alt": "User profile",
111
116
  "sessions": "Sessions",
112
117
  "title": "Your Account",
113
118
  "user_id": "User ID",
@@ -224,6 +229,14 @@
224
229
  "failed": "Login Failed"
225
230
  }
226
231
  },
232
+ "location": {
233
+ "country": "Country",
234
+ "subdivision": "State / province",
235
+ "locality": "City / town",
236
+ "postal_code": "Postal code",
237
+ "street1": "Street address",
238
+ "street2": "Street address line 2"
239
+ },
227
240
  "preference": {
228
241
  "debug": "Debug mode"
229
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.20.2",
3
+ "version": "0.22.0",
4
4
  "author": "James Prevett <jp@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -45,7 +45,7 @@
45
45
  "build": "tsc"
46
46
  },
47
47
  "peerDependencies": {
48
- "@axium/core": ">=0.24.0",
48
+ "@axium/core": ">=0.26.0",
49
49
  "svelte": "^5.36.0",
50
50
  "utilium": "^2.8.0",
51
51
  "zod": "^4.0.5"