@axium/client 0.22.1 → 0.23.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/dist/locales.d.ts CHANGED
@@ -13,11 +13,15 @@ declare let currentLoaded: {
13
13
  readonly toast_removed: "Removed access";
14
14
  readonly public_target: "Everyone";
15
15
  readonly add_public: "Add Public Access";
16
+ readonly placeholder: "Add users and roles";
16
17
  };
17
18
  readonly AppMenu: {
18
19
  readonly failed: "Couldn't load apps.";
19
20
  readonly none: "No apps available.";
20
21
  };
22
+ readonly Discovery: {
23
+ readonly no_results: "No results";
24
+ };
21
25
  readonly Login: {
22
26
  readonly email: "Email";
23
27
  readonly register: "Register instead";
@@ -47,10 +51,6 @@ declare let currentLoaded: {
47
51
  readonly UserCard: {
48
52
  readonly you: "(You)";
49
53
  };
50
- readonly UserDiscovery: {
51
- readonly no_results: "No results";
52
- readonly placeholder: "Add users and roles";
53
- };
54
54
  readonly UserMenu: {
55
55
  readonly account: "Your Account";
56
56
  readonly admin: "Administration";
@@ -247,17 +247,21 @@ declare let currentLoaded: {
247
247
  readonly debug: "Debug mode";
248
248
  };
249
249
  };
250
- export declare let currentMonthNames: string[];
251
250
  /**
252
251
  * Current locale
253
252
  */
254
253
  export declare let currentLocale: string;
254
+ export declare function countryName(code: string): string | undefined;
255
+ export declare function dateField(name: string): string | undefined;
256
+ export declare function conjoin(list: Iterable<string>): string;
257
+ export declare function disjoin(list: Iterable<string>): string;
258
+ export declare let currentMonthNames: string[];
255
259
  type _locale = typeof currentLoaded;
256
260
  export interface Locale extends _locale {
257
261
  }
258
262
  export interface ReplacementOptions {
259
263
  $default?: string;
260
- /** */
264
+ /** Whether to treat the replacement as HTML */
261
265
  $html?: boolean;
262
266
  }
263
267
  type _ArgsValue<V extends string[]> = UnionToIntersection<{
@@ -268,8 +272,6 @@ type _ArgsValue<V extends string[]> = UnionToIntersection<{
268
272
  type Replacements<K extends string> = ReplacementOptions & (GetByString<Locale, K> extends string ? _ArgsValue<Split<GetByString<Locale, K> & string, '{'>> : Record<string, any>);
269
273
  type ReplacementsArgs<K extends string> = {} extends Replacements<K> ? [replacements?: Replacements<K>] : [replacements: Replacements<K>];
270
274
  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;
273
275
  export declare function escape(text: string): string;
274
276
  /**
275
277
  * Get localized text for a given translation key
package/dist/locales.js CHANGED
@@ -15,12 +15,28 @@ export function extendLocale(locale, data) {
15
15
  debug('Extending locale: ' + locale);
16
16
  deepAssign(loadedLocales[locale], data);
17
17
  }
18
- let currentLoaded = en, currentRegionNames, currentDateFields;
19
- export let currentMonthNames;
18
+ let currentLoaded = en;
20
19
  /**
21
20
  * Current locale
22
21
  */
23
22
  export let currentLocale = 'en';
23
+ let currentRegionNames;
24
+ export function countryName(code) {
25
+ return currentRegionNames.of(code);
26
+ }
27
+ let currentDateFields;
28
+ export function dateField(name) {
29
+ return currentDateFields.of(name);
30
+ }
31
+ let currentConjunction;
32
+ export function conjoin(list) {
33
+ return currentConjunction.format(list);
34
+ }
35
+ let currentDisjunction;
36
+ export function disjoin(list) {
37
+ return currentDisjunction.format(list);
38
+ }
39
+ export let currentMonthNames;
24
40
  export function useLocale(newLocale) {
25
41
  if (!loadedLocales[newLocale])
26
42
  throw new Error('Locale is not available: ' + newLocale);
@@ -28,16 +44,12 @@ export function useLocale(newLocale) {
28
44
  currentLoaded = loadedLocales[newLocale];
29
45
  currentRegionNames = new Intl.DisplayNames(newLocale, { type: 'region' });
30
46
  currentDateFields = new Intl.DisplayNames(newLocale, { type: 'dateTimeField' });
47
+ currentConjunction = new Intl.ListFormat(newLocale, { style: 'long', type: 'conjunction' });
48
+ currentDisjunction = new Intl.ListFormat(newLocale, { style: 'long', type: 'disjunction' });
31
49
  const formatter = new Intl.DateTimeFormat(newLocale, { month: 'long' });
32
50
  currentMonthNames = Array.from({ length: 12 }, (_, monthIndex) => formatter.format(new Date(Date.UTC(2000, monthIndex + 1, 1))));
33
51
  }
34
52
  useLocale('en');
35
- export function countryName(code) {
36
- return currentRegionNames.of(code);
37
- }
38
- export function dateField(name) {
39
- return currentDateFields.of(name);
40
- }
41
53
  const localeReplacement = /\{(\w+)\}/g;
42
54
  const escapePattern = /[&<>"']/g;
43
55
  const escapes = {
@@ -1,11 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { addToACL, getACL, removeFromACL, text, updateACL, userInfo } from '@axium/client';
3
- import type { AccessControllable, AccessTarget, UserPublic } from '@axium/core';
3
+ import type { AccessControllable, UserPublic } from '@axium/core';
4
4
  import { checkAndMatchACL, getTarget, pickPermissions } from '@axium/core';
5
5
  import type { HTMLDialogAttributes } from 'svelte/elements';
6
+ import Discovery, * as discovery from './Discovery.svelte';
6
7
  import Icon from './Icon.svelte';
7
8
  import UserCard from './UserCard.svelte';
8
- import UserDiscovery from './UserDiscovery.svelte';
9
9
  import { closeOnBackGesture } from './attachments.js';
10
10
  import { toast, toastStatus } from './toast.js';
11
11
 
@@ -112,8 +112,9 @@
112
112
  </button>
113
113
  {/if}
114
114
  </div>
115
- <UserDiscovery
116
- onSelect={async (target: AccessTarget) => {
115
+
116
+ <Discovery
117
+ onSelect={async ({ target }) => {
117
118
  try {
118
119
  const control = await addToACL(itemType, item.id, target);
119
120
  if (control.userId) control.user = await userInfo(control.userId);
@@ -122,7 +123,13 @@
122
123
  toast('error', e);
123
124
  }
124
125
  }}
125
- excludeTargets={acl.map(getTarget).filter<string>((t): t is string => !!t)}
126
+ sources={[discovery.user, discovery.role]}
127
+ exclude={result =>
128
+ acl
129
+ .map(getTarget)
130
+ .filter<string>((t): t is string => !!t)
131
+ .includes(result.target)}
132
+ placeholder={text('AccessControlDialog.placeholder')}
126
133
  />
127
134
  {/if}
128
135
 
@@ -0,0 +1,179 @@
1
+ <script lang="ts" module>
2
+ import { fetchAPI } from '@axium/client';
3
+ import type { UserPublic } from '@axium/core';
4
+
5
+ export interface Source<T> {
6
+ name: string;
7
+ get(search: string): T[] | Promise<T[]>;
8
+ render: Snippet<[T]>;
9
+ }
10
+
11
+ // built-in / common discovery sources
12
+ // note getters are used because svelte declares the snippets after the module script
13
+
14
+ export const user: Source<{ type: 'user'; user: UserPublic; target: string }> = {
15
+ name: 'user',
16
+ async get(value) {
17
+ const users = await fetchAPI('POST', 'users/discover', value);
18
+ return users.map(user => ({ type: 'user', user, target: user.id }));
19
+ },
20
+ get render() {
21
+ return renderUser;
22
+ },
23
+ };
24
+
25
+ export const role: Source<{ type: 'role'; role: string; target: string }> = {
26
+ name: 'role',
27
+ get: role => [{ type: 'role', role, target: '@' + role }],
28
+ get render() {
29
+ return renderRole;
30
+ },
31
+ };
32
+
33
+ export const exact: Source<{ type: 'exact'; target: string }> = {
34
+ name: 'exact',
35
+ get: value => [{ type: 'exact', target: value }],
36
+ get render() {
37
+ return renderExact;
38
+ },
39
+ };
40
+ </script>
41
+
42
+ <script lang="ts" generics="T extends any[]">
43
+ import { text } from '@axium/client';
44
+ import { errorText } from 'ioium';
45
+ import type { Snippet } from 'svelte';
46
+ import Icon from './Icon.svelte';
47
+ import UserCard from './UserCard.svelte';
48
+
49
+ type Value = T[keyof T & number];
50
+
51
+ let {
52
+ onSelect,
53
+ sources,
54
+ exclude,
55
+ placeholder,
56
+ initialValue = '',
57
+ }: {
58
+ onSelect(target: Value): string | void | Promise<string | void>;
59
+ sources: { [K in keyof T]: Source<T[K]> };
60
+ exclude?(target: Value): boolean;
61
+ placeholder?: string;
62
+ initialValue?: string;
63
+ } = $props();
64
+
65
+ type Result = { [K in keyof T]: { value: T[K]; snippet: Snippet<[T[K]]> } }[keyof T];
66
+ let results = $state<Result[]>([]),
67
+ value = $state<string>(initialValue),
68
+ gotErrors = $state<Record<string, boolean>>({});
69
+
70
+ const allError = $derived(Object.values(gotErrors).every(v => v));
71
+
72
+ async function oninput() {
73
+ results = [];
74
+ if (!value || !value.length) return;
75
+
76
+ for (const source of sources) {
77
+ if (gotErrors[source.name]) continue;
78
+
79
+ try {
80
+ for (const result of await source.get(value)) {
81
+ if (exclude?.(result)) continue;
82
+ results.push({ value: result, snippet: source.render });
83
+ }
84
+ gotErrors[source.name] = false;
85
+ } catch (e) {
86
+ console.warn(`Can not discover ${source.name}:`, errorText(e));
87
+ gotErrors[source.name] = true;
88
+ }
89
+ }
90
+ }
91
+
92
+ function select(target: Value) {
93
+ return async (e: Event) => {
94
+ e.stopPropagation();
95
+ try {
96
+ value = (await onSelect(target)) || '';
97
+ } catch (e) {
98
+ console.log('onSelect error:', e);
99
+ }
100
+ results = [];
101
+ };
102
+ }
103
+ </script>
104
+
105
+ {#snippet renderUser({ user }: { user: UserPublic; target: string })}
106
+ <UserCard {user} />
107
+ {/snippet}
108
+
109
+ {#snippet renderRole({ role }: { role: string; target: string })}
110
+ <span class="icon-text"><Icon i="at" />{role}</span>
111
+ {/snippet}
112
+
113
+ {#snippet renderExact(result: { target: string })}
114
+ <span>{result.target}</span>
115
+ {/snippet}
116
+
117
+ <input bind:value type="text" {placeholder} {oninput} />
118
+ {#if !allError && value}
119
+ <div class="results">
120
+ {#each results as { value, snippet }}
121
+ <div class="result" onclick={select(value)}>{@render snippet(value)}</div>
122
+ {:else}
123
+ <i>{text('Discovery.no_results')}</i>
124
+ {/each}
125
+ </div>
126
+ {/if}
127
+
128
+ <style>
129
+ :host {
130
+ anchor-scope: --discovery-input;
131
+ }
132
+
133
+ input {
134
+ anchor-name: --discovery-input;
135
+ }
136
+
137
+ input:focus + .results,
138
+ .results:active {
139
+ display: flex;
140
+ animation: var(--A-zoom);
141
+ }
142
+
143
+ .results {
144
+ position: fixed;
145
+ position-anchor: --discovery-input;
146
+ inset: anchor(bottom) anchor(right) auto anchor(left);
147
+ display: none;
148
+ flex-direction: column;
149
+ gap: 0.25em;
150
+ height: fit-content;
151
+ max-height: 25em;
152
+ background-color: var(--bg-accent);
153
+ border-radius: 0.25em 0.25em 0.75em 0.75em;
154
+ padding: 1em;
155
+ border: var(--border-accent);
156
+ align-items: stretch;
157
+
158
+ i {
159
+ text-align: center;
160
+ }
161
+ }
162
+
163
+ .result {
164
+ border-radius: 1em;
165
+ padding: 0.25em 0.75em;
166
+ gap: 0.25em;
167
+ display: inline-flex;
168
+ align-items: center;
169
+
170
+ :global(& > *) {
171
+ padding: 0.5em;
172
+ }
173
+ }
174
+
175
+ .result:hover {
176
+ cursor: pointer;
177
+ background-color: var(--bg-strong);
178
+ }
179
+ </style>
@@ -26,7 +26,7 @@
26
26
  {#if !compact}
27
27
  <UserPFP {user} />
28
28
  {/if}
29
- {user.name}
29
+ <span>{user.name}</span>
30
30
  {#if self && you}
31
31
  <span class="subtle">{text('UserCard.you')}</span>
32
32
  {/if}
@@ -37,5 +37,8 @@
37
37
  cursor: pointer;
38
38
  width: max-content;
39
39
  height: max-content;
40
+ gap: 0.25em;
41
+ display: inline-flex;
42
+ align-items: center;
40
43
  }
41
44
  </style>
package/lib/index.ts CHANGED
@@ -2,6 +2,8 @@ export { default as AccessControlDialog } from './AccessControlDialog.svelte';
2
2
  export { default as AppMenu } from './AppMenu.svelte';
3
3
  export { default as ColorPicker } from './ColorPicker.svelte';
4
4
  export { default as ClipboardCopy } from './ClipboardCopy.svelte';
5
+ export { default as Discovery } from './Discovery.svelte';
6
+ export * as discovery from './Discovery.svelte';
5
7
  export { default as FormDialog } from './FormDialog.svelte';
6
8
  export { default as Icon } from './Icon.svelte';
7
9
  export { default as LocationSelect } from './LocationSelect.svelte';
@@ -15,7 +17,6 @@ export { default as SidebarLayout } from './SidebarLayout.svelte';
15
17
  export { default as Upload } from './Upload.svelte';
16
18
  export { default as URLText } from './URLText.svelte';
17
19
  export { default as UserCard } from './UserCard.svelte';
18
- export { default as UserDiscovery } from './UserDiscovery.svelte';
19
20
  export { default as UserPFP } from './UserPFP.svelte';
20
21
  export { default as UserMenu } from './UserMenu.svelte';
21
22
  export { default as Version } from './Version.svelte';
package/locales/en.json CHANGED
@@ -6,12 +6,16 @@
6
6
  "remove": "Remove",
7
7
  "toast_removed": "Removed access",
8
8
  "public_target": "Everyone",
9
- "add_public": "Add Public Access"
9
+ "add_public": "Add Public Access",
10
+ "placeholder": "Add users and roles"
10
11
  },
11
12
  "AppMenu": {
12
13
  "failed": "Couldn't load apps.",
13
14
  "none": "No apps available."
14
15
  },
16
+ "Discovery": {
17
+ "no_results": "No results"
18
+ },
15
19
  "Login": {
16
20
  "email": "Email",
17
21
  "register": "Register instead"
@@ -41,10 +45,6 @@
41
45
  "UserCard": {
42
46
  "you": "(You)"
43
47
  },
44
- "UserDiscovery": {
45
- "no_results": "No results",
46
- "placeholder": "Add users and roles"
47
- },
48
48
  "UserMenu": {
49
49
  "account": "Your Account",
50
50
  "admin": "Administration"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "author": "James Prevett <jp@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -1,139 +0,0 @@
1
- <script lang="ts">
2
- import { text } from '@axium/client';
3
- import { fetchAPI } from '@axium/client/requests';
4
- import type { UserPublic } from '@axium/core';
5
- import { errorText } from 'ioium';
6
- import Icon from './Icon.svelte';
7
- import UserPFP from './UserPFP.svelte';
8
-
9
- const {
10
- onSelect,
11
- enableTags = false,
12
- excludeTargets = [],
13
- noRoles = false,
14
- allowExact,
15
- }: {
16
- onSelect(target: string): unknown;
17
- enableTags?: boolean;
18
- excludeTargets?: string[];
19
- noRoles?: boolean;
20
- allowExact?: boolean;
21
- } = $props();
22
-
23
- type Result = { type: 'user'; value: UserPublic; target: string } | { type: 'role' | 'tag' | 'exact'; value: string; target: string };
24
- let results = $state<Result[]>([]);
25
- let value = $state<string>();
26
- let gotError = $state<boolean>(false);
27
-
28
- async function oninput() {
29
- if (!value || !value.length) {
30
- results = [];
31
- return;
32
- }
33
-
34
- try {
35
- const users = await fetchAPI('POST', 'users/discover', value);
36
- results = [
37
- allowExact && ({ type: 'exact', value, target: value } as const),
38
- ...users.map(value => ({ type: 'user', value, target: value.id }) as const),
39
- !noRoles && ({ type: 'role', value, target: '@' + value } as const),
40
- enableTags && ({ type: 'tag', value, target: '#' + value } as const),
41
- ].filter<Result>(r => !!r);
42
- } catch (e) {
43
- gotError = true;
44
- console.warn('Can not use user discovery:', errorText(e));
45
- results = [];
46
- }
47
- }
48
-
49
- function select(target: string) {
50
- return (e: Event) => {
51
- e.stopPropagation();
52
- onSelect(target);
53
- results = [];
54
- value = '';
55
- };
56
- }
57
- </script>
58
-
59
- <input bind:value type="text" placeholder={text('UserDiscovery.placeholder')} {oninput} />
60
- {#if !gotError && value}
61
- <!-- Don't show results when we can't use the discovery API -->
62
- <div class="results">
63
- {#each results as result}
64
- {#if !excludeTargets.includes(result.target)}
65
- <div class="result" onclick={select(result.target)}>
66
- {#if result.type == 'user'}
67
- <span><UserPFP user={result.value} /> {result.value.name}</span>
68
- {:else if result.type == 'role'}
69
- <span>
70
- <span class="icon-text non-user"><Icon i="at" />{result.value}</span>
71
- </span>
72
- {:else if result.type == 'tag'}
73
- <span>
74
- <span class="icon-text non-user"><Icon i="hashtag" />{result.value}</span>
75
- </span>
76
- {:else if result.type == 'exact'}
77
- <span class="non-user">{result.value}</span>
78
- {/if}
79
- </div>
80
- {/if}
81
- {:else}
82
- <i>{text('UserDiscovery.no_results')}</i>
83
- {/each}
84
- </div>
85
- {/if}
86
-
87
- <style>
88
- :host {
89
- anchor-scope: --discovery-input;
90
- }
91
-
92
- input {
93
- anchor-name: --discovery-input;
94
- }
95
-
96
- input:focus + .results,
97
- .results:active {
98
- display: flex;
99
- animation: var(--A-zoom);
100
- }
101
-
102
- .results {
103
- position: fixed;
104
- position-anchor: --discovery-input;
105
- inset: anchor(bottom) anchor(right) auto anchor(left);
106
- display: none;
107
- flex-direction: column;
108
- gap: 0.25em;
109
- height: fit-content;
110
- max-height: 25em;
111
- background-color: var(--bg-accent);
112
- border-radius: 0.25em 0.25em 0.75em 0.75em;
113
- padding: 1em;
114
- border: var(--border-accent);
115
- align-items: stretch;
116
-
117
- i {
118
- text-align: center;
119
- }
120
- }
121
-
122
- .result {
123
- padding: 0.5em;
124
- border-radius: 0.5em;
125
-
126
- .non-user {
127
- border-radius: 1em;
128
- padding: 0.25em 0.75em;
129
- display: inline-flex;
130
- align-items: center;
131
- gap: 0.25em;
132
- }
133
- }
134
-
135
- .result:hover {
136
- cursor: pointer;
137
- background-color: var(--bg-strong);
138
- }
139
- </style>