@axium/client 0.17.3 → 0.18.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/animations.css +44 -5
- package/assets/styles.css +62 -18
- package/assets/theme.css +10 -0
- package/dist/gui/animate.d.ts +8 -0
- package/dist/gui/animate.js +27 -0
- package/dist/gui/index.d.ts +2 -0
- package/dist/gui/index.js +2 -0
- package/dist/locales.d.ts +3 -0
- package/lib/AccessControlDialog.svelte +52 -20
- package/lib/ClipboardCopy.svelte +1 -1
- package/lib/FormDialog.svelte +1 -1
- package/lib/UserDiscovery.svelte +2 -6
- package/lib/ZodInput.svelte +5 -4
- package/lib/attachments.ts +4 -0
- package/lib/index.ts +0 -1
- package/lib/toast.ts +81 -0
- package/locales/en.json +4 -1
- package/package.json +2 -1
- package/lib/Toast.svelte +0 -35
- /package/dist/{clipboard.d.ts → gui/clipboard.d.ts} +0 -0
- /package/dist/{clipboard.js → gui/clipboard.js} +0 -0
package/assets/animations.css
CHANGED
|
@@ -16,15 +16,54 @@
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
@keyframes fade-subtle {
|
|
20
|
+
from {
|
|
21
|
+
opacity: 0.75;
|
|
22
|
+
}
|
|
23
|
+
to {
|
|
24
|
+
opacity: 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes slide-up {
|
|
29
|
+
from {
|
|
30
|
+
translate: 0 var(--A-slide-amount, 100%);
|
|
31
|
+
}
|
|
32
|
+
to {
|
|
33
|
+
translate: 0 0;
|
|
34
|
+
}
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
@keyframes slide-right {
|
|
38
|
+
from {
|
|
39
|
+
translate: 0 0;
|
|
40
|
+
}
|
|
41
|
+
to {
|
|
42
|
+
translate: var(--A-slide-amount, 100%) 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media not (prefers-reduced-motion) {
|
|
47
|
+
:root {
|
|
48
|
+
/* CSS variables to make using the animations easier.
|
|
49
|
+
Prefixed with `A-` to avoid namespace pollution. */
|
|
50
|
+
--A-zoom: zoom 0.25s cubic-bezier(0.35, 1.55, 0.65, 1);
|
|
51
|
+
--A-fade: fade 0.25s ease-out;
|
|
52
|
+
--A-slide-up: slide-up 0.25s ease;
|
|
53
|
+
--A-slide-right: slide-right 0.25s ease;
|
|
54
|
+
--A-slide-out-right: slide-right 0.25s ease forwards, var(--A-fade) reverse forwards;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dialog:open {
|
|
25
59
|
animation: var(--A-zoom);
|
|
26
60
|
}
|
|
27
61
|
|
|
28
|
-
dialog
|
|
62
|
+
dialog:open::backdrop {
|
|
29
63
|
animation: var(--A-fade);
|
|
30
64
|
}
|
|
65
|
+
|
|
66
|
+
.toast {
|
|
67
|
+
--A-slide-amount: 100px;
|
|
68
|
+
animation: var(--A-slide-up);
|
|
69
|
+
}
|
package/assets/styles.css
CHANGED
|
@@ -68,11 +68,23 @@ select {
|
|
|
68
68
|
padding: 0.5em 1em;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
button {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
|
|
74
|
+
&:hover {
|
|
75
|
+
background-color: hsl(var(--hue) 15 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
button.reset {
|
|
72
80
|
border-radius: unset;
|
|
73
81
|
border: none;
|
|
74
82
|
background-color: unset;
|
|
75
83
|
padding: unset;
|
|
84
|
+
|
|
85
|
+
&:hover {
|
|
86
|
+
background-color: unset;
|
|
87
|
+
}
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
input,
|
|
@@ -116,14 +128,6 @@ option:hover {
|
|
|
116
128
|
background-color: var(--bg-alt);
|
|
117
129
|
}
|
|
118
130
|
|
|
119
|
-
button {
|
|
120
|
-
cursor: pointer;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
button:hover {
|
|
124
|
-
background-color: hsl(var(--hue) 15 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
131
|
code,
|
|
128
132
|
pre {
|
|
129
133
|
background-color: var(--bg-menu);
|
|
@@ -144,7 +148,7 @@ dialog {
|
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
dialog::backdrop {
|
|
151
|
+
dialog:modal::backdrop {
|
|
148
152
|
background: #0003;
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -178,24 +182,33 @@ progress::-moz-progress-bar {
|
|
|
178
182
|
background-color: var(--bg-accent);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
:
|
|
185
|
+
:is(p, span, i).error {
|
|
186
|
+
color: var(--fg-error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
input.error {
|
|
190
|
+
border: var(--border-error);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
div.error {
|
|
182
194
|
padding: 1em;
|
|
183
195
|
border-radius: 0.5em;
|
|
184
196
|
background-color: var(--bg-error);
|
|
197
|
+
border: var(--border-error);
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
border: 1px solid var(--bg-error);
|
|
200
|
+
div.warning {
|
|
201
|
+
padding: 1em;
|
|
202
|
+
border-radius: 0.5em;
|
|
203
|
+
background-color: var(--bg-warning);
|
|
204
|
+
border: var(--border-warning);
|
|
193
205
|
}
|
|
194
206
|
|
|
195
|
-
.success {
|
|
207
|
+
div.success {
|
|
196
208
|
padding: 1em;
|
|
197
209
|
border-radius: 0.5em;
|
|
198
210
|
background-color: var(--bg-success);
|
|
211
|
+
border: var(--border-success);
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
.subtle {
|
|
@@ -215,6 +228,7 @@ input.error {
|
|
|
215
228
|
border: var(--border-error);
|
|
216
229
|
background-color: hsl(0 20 calc(var(--bg-light) + var(--light-step)));
|
|
217
230
|
color: hsl(0 33 var(--fg-light));
|
|
231
|
+
--fill: hsl(0 33 var(--fg-light));
|
|
218
232
|
accent-color: hsl(0 33 var(--fg-light));
|
|
219
233
|
}
|
|
220
234
|
|
|
@@ -308,6 +322,36 @@ h6 {
|
|
|
308
322
|
}
|
|
309
323
|
}
|
|
310
324
|
|
|
325
|
+
#toasts {
|
|
326
|
+
position: absolute;
|
|
327
|
+
right: 1em;
|
|
328
|
+
top: 1em;
|
|
329
|
+
width: 0;
|
|
330
|
+
height: 0;
|
|
331
|
+
overflow: visible;
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
gap: 1em;
|
|
335
|
+
text-align: right;
|
|
336
|
+
align-items: end;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
div.toast {
|
|
340
|
+
width: max-content;
|
|
341
|
+
padding: 0.4em 0.8em;
|
|
342
|
+
border-radius: 0.5em;
|
|
343
|
+
|
|
344
|
+
&:hover {
|
|
345
|
+
animation-play-state: paused;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
&.plain,
|
|
349
|
+
&.info {
|
|
350
|
+
border: var(--border-accent);
|
|
351
|
+
background-color: var(--bg-accent);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
311
355
|
@media (width >= 700px) {
|
|
312
356
|
.mobile-only {
|
|
313
357
|
display: none;
|
|
@@ -333,7 +377,7 @@ h6 {
|
|
|
333
377
|
}
|
|
334
378
|
|
|
335
379
|
.mobile-button:hover {
|
|
336
|
-
background-color:
|
|
380
|
+
background-color: var(--bg-accent);
|
|
337
381
|
}
|
|
338
382
|
|
|
339
383
|
.mobile-float-left {
|
package/assets/theme.css
CHANGED
|
@@ -35,10 +35,20 @@ html {
|
|
|
35
35
|
--bg-accent: hsl(var(--hue) 15 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
36
36
|
--bg-alt: hsl(var(--hue) 5 calc(var(--bg-light) + var(--light-step)));
|
|
37
37
|
--bg-strong: hsl(var(--hue) 20 calc(var(--bg-light) + var(--light-step)));
|
|
38
|
+
|
|
38
39
|
--bg-error: hsl(0 40 var(--bg-light-status));
|
|
40
|
+
--bg-warning: hsl(60 40 var(--bg-light-status));
|
|
39
41
|
--bg-success: hsl(120 40 var(--bg-light-status));
|
|
42
|
+
|
|
40
43
|
--fg-accent: hsl(var(--hue) 15 var(--fg-light));
|
|
41
44
|
--fg-strong: hsl(var(--hue) 25 calc(var(--fg-light) - var(--light-step)));
|
|
45
|
+
|
|
46
|
+
--fg-error: hsl(0 50 calc(var(--fg-light) - var(--light-step)));
|
|
47
|
+
--fg-warning: hsl(60 50 calc(var(--fg-light) - var(--light-step)));
|
|
48
|
+
|
|
42
49
|
--border-accent: 1px solid hsl(var(--hue) 10 calc(var(--bg-light) + (var(--light-step) * 3)));
|
|
50
|
+
|
|
43
51
|
--border-error: 1px solid hsl(0 50 var(--fg-light));
|
|
52
|
+
--border-warning: 1px solid hsl(60 50 var(--fg-light));
|
|
53
|
+
--border-success: 1px solid hsl(120 50 var(--fg-light));
|
|
44
54
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether an element currently has an animation.
|
|
3
|
+
* This will be empty or `none` when prefers-reduced-motion is enabled
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasAnimation(element: HTMLElement): boolean;
|
|
6
|
+
export declare function onAnimationEnd(element: HTMLElement): Promise<void>;
|
|
7
|
+
/** Waits for an animation to complete on an element, after any other animations. */
|
|
8
|
+
export declare function animate(element: HTMLElement, animation: string): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether an element currently has an animation.
|
|
3
|
+
* This will be empty or `none` when prefers-reduced-motion is enabled
|
|
4
|
+
*/
|
|
5
|
+
export function hasAnimation(element) {
|
|
6
|
+
const { animationName } = getComputedStyle(element);
|
|
7
|
+
return !!animationName && animationName !== 'none';
|
|
8
|
+
}
|
|
9
|
+
const pending = new WeakMap();
|
|
10
|
+
export function onAnimationEnd(element) {
|
|
11
|
+
if (!hasAnimation(element))
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
14
|
+
element.addEventListener('animationend', () => resolve(), { once: true });
|
|
15
|
+
element.addEventListener('animationcancel', () => resolve(), { once: true });
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
/** Waits for an animation to complete on an element, after any other animations. */
|
|
19
|
+
export async function animate(element, animation) {
|
|
20
|
+
await pending.get(element);
|
|
21
|
+
element.style.animation = animation;
|
|
22
|
+
const ended = onAnimationEnd(element);
|
|
23
|
+
pending.set(element, ended);
|
|
24
|
+
await ended;
|
|
25
|
+
if (pending.get(element) === ended)
|
|
26
|
+
pending.delete(element);
|
|
27
|
+
}
|
package/dist/locales.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ declare let currentLoaded: {
|
|
|
10
10
|
readonly named_title: "Permissions for <strong>{name}</strong>";
|
|
11
11
|
readonly owner: "Owner";
|
|
12
12
|
readonly title: "Permissions";
|
|
13
|
+
readonly remove: "Remove";
|
|
14
|
+
readonly toast_removed: "Removed access";
|
|
13
15
|
};
|
|
14
16
|
readonly AppMenu: {
|
|
15
17
|
readonly failed: "Couldn't load apps.";
|
|
@@ -78,6 +80,7 @@ declare let currentLoaded: {
|
|
|
78
80
|
readonly preferences: "Preferences";
|
|
79
81
|
readonly register: "Register";
|
|
80
82
|
readonly sessions: "Sessions";
|
|
83
|
+
readonly success: "Success";
|
|
81
84
|
readonly unknown: "Unknown";
|
|
82
85
|
readonly unnamed: "Unnamed";
|
|
83
86
|
readonly username: "Name";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { addToACL, getACL, text, updateACL, userInfo } from '@axium/client';
|
|
2
|
+
import { addToACL, getACL, removeFromACL, text, updateACL, userInfo } from '@axium/client';
|
|
3
3
|
import type { AccessControllable, AccessTarget, User } from '@axium/core';
|
|
4
4
|
import { getTarget, pickPermissions } from '@axium/core';
|
|
5
5
|
import { errorText } from '@axium/core/io';
|
|
6
|
+
import { toastStatus } from './toast.js';
|
|
6
7
|
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
7
8
|
import Icon from './Icon.svelte';
|
|
8
9
|
import UserCard from './UserCard.svelte';
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
}
|
|
28
29
|
</script>
|
|
29
30
|
|
|
30
|
-
<dialog bind:this={dialog} {...rest}>
|
|
31
|
+
<dialog bind:this={dialog} {...rest} onclick={e => e.stopPropagation()}>
|
|
31
32
|
{#if item.name}
|
|
32
33
|
<h3>{@html text('component.AccessControlDialog.named_title', { $html: true, name: item.name })}</h3>
|
|
33
34
|
{:else}
|
|
@@ -38,16 +39,16 @@
|
|
|
38
39
|
<div class="error">{error}</div>
|
|
39
40
|
{/if}
|
|
40
41
|
|
|
41
|
-
<div class="AccessControl">
|
|
42
|
+
<div class="AccessControl text">
|
|
42
43
|
{#if item.user}
|
|
43
44
|
<UserCard user={item.user} />
|
|
44
45
|
{:else if item}
|
|
45
|
-
{#await userInfo(item.userId) then user}<UserCard {user} />{/await}
|
|
46
|
+
{#await userInfo(item.userId) then user}<UserCard {user} />{:catch}<i>{text('generic.unknown')}</i>{/await}
|
|
46
47
|
{/if}
|
|
47
48
|
<span>{text('component.AccessControlDialog.owner')}</span>
|
|
48
49
|
</div>
|
|
49
50
|
|
|
50
|
-
{#each acl as control}
|
|
51
|
+
{#each acl as control, i (control.userId || control.role || control.tag)}
|
|
51
52
|
{@const update = (key: string) => async (e: Event & { currentTarget: HTMLInputElement }) => {
|
|
52
53
|
try {
|
|
53
54
|
const updated = await updateACL(itemType, item.id, getTarget(control), { [key]: e.currentTarget.checked });
|
|
@@ -57,16 +58,36 @@
|
|
|
57
58
|
}
|
|
58
59
|
}}
|
|
59
60
|
<div class="AccessControl">
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
<div class="target">
|
|
62
|
+
{#if control.user}
|
|
63
|
+
<UserCard user={control.user} />
|
|
64
|
+
{:else if control.role}
|
|
65
|
+
<span class="icon-text">
|
|
66
|
+
<Icon i="at" />
|
|
67
|
+
<span>{control.role}</span>
|
|
68
|
+
</span>
|
|
69
|
+
{:else if control.tag}
|
|
70
|
+
<span class="icon-text">
|
|
71
|
+
<Icon i="hashtag" />
|
|
72
|
+
<span>{control.tag}</span>
|
|
73
|
+
</span>
|
|
74
|
+
{:else}
|
|
75
|
+
<i>{text('generic.unknown')}</i>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if editable}
|
|
78
|
+
<button
|
|
79
|
+
class="icon-text danger"
|
|
80
|
+
onclick={() =>
|
|
81
|
+
toastStatus(
|
|
82
|
+
removeFromACL(itemType, item.id, getTarget(control)).then(() => acl.splice(i, 1)),
|
|
83
|
+
text('component.AccessControlDialog.toast_removed')
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<Icon i="user-minus" />
|
|
87
|
+
<span>{text('component.AccessControlDialog.remove')}</span>
|
|
88
|
+
</button>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
70
91
|
<div class="permissions">
|
|
71
92
|
{#each Object.entries(pickPermissions(control) as Record<string, boolean>) as [key, value]}
|
|
72
93
|
{@const id = `${item.id}.${getTarget(control)}.${key}`}
|
|
@@ -79,7 +100,7 @@
|
|
|
79
100
|
{:else}
|
|
80
101
|
<Icon i={value ? 'check' : 'xmark'} />
|
|
81
102
|
{/if}
|
|
82
|
-
<span>{key}</span>
|
|
103
|
+
<span>{text(`permission.${itemType}.${key}`, { $default: key })}</span>
|
|
83
104
|
</span>
|
|
84
105
|
{/each}
|
|
85
106
|
</div>
|
|
@@ -113,12 +134,23 @@
|
|
|
113
134
|
.AccessControl {
|
|
114
135
|
display: grid;
|
|
115
136
|
gap: 1em;
|
|
116
|
-
grid-template-columns: 1fr
|
|
117
|
-
min-width: 30em;
|
|
137
|
+
grid-template-columns: 1fr 1fr;
|
|
118
138
|
padding: 1em 2em;
|
|
139
|
+
border-bottom: var(--border-accent);
|
|
140
|
+
|
|
141
|
+
&.text {
|
|
142
|
+
align-items: center;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.target {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
justify-content: space-around;
|
|
149
|
+
gap: 1em;
|
|
150
|
+
}
|
|
119
151
|
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
@media (width > 700px) {
|
|
153
|
+
min-width: 30em;
|
|
122
154
|
}
|
|
123
155
|
}
|
|
124
156
|
</style>
|
package/lib/ClipboardCopy.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { fade } from 'svelte/transition';
|
|
3
3
|
import { wait } from 'utilium';
|
|
4
4
|
import Icon from './Icon.svelte';
|
|
5
|
-
import * as clip from '@axium/client/
|
|
5
|
+
import * as clip from '@axium/client/gui';
|
|
6
6
|
|
|
7
7
|
const { value, type = 'text/plain' }: { value: BlobPart; type?: string } = $props();
|
|
8
8
|
|
package/lib/FormDialog.svelte
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
<button type="submit" class={['submit', submitDanger && 'danger']}>{submitText}</button>
|
|
63
63
|
{/snippet}
|
|
64
64
|
|
|
65
|
-
<dialog bind:this={dialog} {onclose} {...rest}>
|
|
65
|
+
<dialog bind:this={dialog} {onclose} {...rest} onclick={e => e.stopPropagation()}>
|
|
66
66
|
{@render header?.()}
|
|
67
67
|
<form {onsubmit} class="main" method="dialog">
|
|
68
68
|
{#if error}
|
package/lib/UserDiscovery.svelte
CHANGED
|
@@ -67,15 +67,11 @@
|
|
|
67
67
|
<span><img src={getUserImage(result.value)} alt={result.value.name} />{result.value.name}</span>
|
|
68
68
|
{:else if result.type == 'role'}
|
|
69
69
|
<span>
|
|
70
|
-
<span class="icon-text
|
|
71
|
-
><Icon i="at" />{result.value}</span
|
|
72
|
-
>
|
|
70
|
+
<span class="icon-text non-user"><Icon i="at" />{result.value}</span>
|
|
73
71
|
</span>
|
|
74
72
|
{:else if result.type == 'tag'}
|
|
75
73
|
<span>
|
|
76
|
-
<span class="icon-text
|
|
77
|
-
><Icon i="hashtag" />{result.value}</span
|
|
78
|
-
>
|
|
74
|
+
<span class="icon-text non-user"><Icon i="hashtag" />{result.value}</span>
|
|
79
75
|
</span>
|
|
80
76
|
{:else if result.type == 'exact'}
|
|
81
77
|
<span class="non-user">{result.value}</span>
|
package/lib/ZodInput.svelte
CHANGED
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
{#snippet _in(rest: HTMLInputAttributes)}
|
|
91
91
|
<div class="ZodInput">
|
|
92
92
|
{#if !noLabel}<label for={id}>{labelText}</label>{/if}
|
|
93
|
-
{#if error}<span class="ZodInput-error
|
|
93
|
+
{#if error}<span class="ZodInput-error">{error}</span>{/if}
|
|
94
94
|
<input {id} {...rest} bind:value {onchange} {oninput} required={!optional} {defaultValue} class={[error && 'error']} />
|
|
95
95
|
</div>
|
|
96
96
|
{/snippet}
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
{:else if schema.type == 'array'}
|
|
137
137
|
<div class="ZodInput">
|
|
138
138
|
{#if !noLabel}<label for={id}>{labelText}</label>{/if}
|
|
139
|
-
{#if error}<span class="ZodInput-error
|
|
139
|
+
{#if error}<span class="ZodInput-error">{error}</span>{/if}
|
|
140
140
|
<div class="ZodInput-array">
|
|
141
141
|
{#each value, i}
|
|
142
142
|
<div class="ZodInput-element">
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
{:else if schema.type == 'enum'}
|
|
181
181
|
<div class="ZodInput">
|
|
182
182
|
{#if !noLabel}<label for={id}>{labelText}</label>{/if}
|
|
183
|
-
{#if error}<span class="ZodInput-error
|
|
183
|
+
{#if error}<span class="ZodInput-error">{error}</span>{/if}
|
|
184
184
|
<select {id} {onchange} bind:value required={!optional}>
|
|
185
185
|
{#each Object.entries(schema.enum) as [key, value]}
|
|
186
186
|
<option {value} selected={value === value}>{key}</option>
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
</div>
|
|
190
190
|
{:else}
|
|
191
191
|
<!-- No idea how to render this -->
|
|
192
|
-
<i class="error
|
|
192
|
+
<i class="error">{text('component.ZodInput.invalid_type', { type: JSON.stringify((schema as ZodPref)?.def?.type) })}</i>
|
|
193
193
|
{/if}
|
|
194
194
|
|
|
195
195
|
<style>
|
|
@@ -198,6 +198,7 @@
|
|
|
198
198
|
position-anchor: --zod-input;
|
|
199
199
|
bottom: calc(anchor(top) - 0.3em);
|
|
200
200
|
left: anchor(left);
|
|
201
|
+
color: var(--fg-error);
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
.ZodInput {
|
package/lib/attachments.ts
CHANGED
|
@@ -49,6 +49,10 @@ export function contextMenu(...menuItems: (ContextMenuItem | false | null | unde
|
|
|
49
49
|
let _forcePopover = false;
|
|
50
50
|
|
|
51
51
|
element.oncontextmenu = (e: MouseEvent) => {
|
|
52
|
+
for (let node = e.target as HTMLElement | null; node && node !== element; node = node.parentElement) {
|
|
53
|
+
if (node instanceof HTMLDialogElement || (node.popover && node !== menu)) return;
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
e.preventDefault();
|
|
53
57
|
e.stopPropagation();
|
|
54
58
|
|
package/lib/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ export { default as Popover } from './Popover.svelte';
|
|
|
11
11
|
export { default as Register } from './Register.svelte';
|
|
12
12
|
export { default as SessionList } from './SessionList.svelte';
|
|
13
13
|
export { default as SidebarLayout } from './SidebarLayout.svelte';
|
|
14
|
-
export { default as Toast } from './Toast.svelte';
|
|
15
14
|
export { default as Upload } from './Upload.svelte';
|
|
16
15
|
export { default as URLText } from './URLText.svelte';
|
|
17
16
|
export { default as UserCard } from './UserCard.svelte';
|
package/lib/toast.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { debug, errorText } from '@axium/core/io';
|
|
2
|
+
import { text } from '@axium/client/locales';
|
|
3
|
+
import { animate } from '@axium/client/gui';
|
|
4
|
+
import Icon from './Icon.svelte';
|
|
5
|
+
import { mount } from 'svelte';
|
|
6
|
+
|
|
7
|
+
const list = document.querySelector<HTMLDivElement>('#toasts')!;
|
|
8
|
+
|
|
9
|
+
const toastIcons = {
|
|
10
|
+
success: 'check',
|
|
11
|
+
warning: 'regular/triangle-exclamation',
|
|
12
|
+
error: 'octagon-xmark',
|
|
13
|
+
info: 'regular/circle-info',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Used to determine icon and styling */
|
|
17
|
+
export type ToastType = 'plain' | keyof typeof toastIcons;
|
|
18
|
+
|
|
19
|
+
const durationMultiplier = {
|
|
20
|
+
warning: 1.25,
|
|
21
|
+
error: 1.5,
|
|
22
|
+
} as Record<ToastType, number>;
|
|
23
|
+
|
|
24
|
+
export async function toast(type: ToastType, message: any): Promise<void> {
|
|
25
|
+
const text = errorText(message);
|
|
26
|
+
|
|
27
|
+
const toast = document.createElement('div');
|
|
28
|
+
toast.classList.add('toast', 'icon-text', type);
|
|
29
|
+
if (type != 'plain') mount(Icon, { target: toast, props: { i: toastIcons[type] } });
|
|
30
|
+
|
|
31
|
+
const span = document.createElement('span');
|
|
32
|
+
span.textContent = text;
|
|
33
|
+
toast.appendChild(span);
|
|
34
|
+
|
|
35
|
+
list.appendChild(toast);
|
|
36
|
+
|
|
37
|
+
async function dismiss() {
|
|
38
|
+
debug('Toast dismissed');
|
|
39
|
+
await animate(toast, 'var(--A-slide-out-right)');
|
|
40
|
+
toast.remove();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let persisted = false;
|
|
44
|
+
|
|
45
|
+
function persist() {
|
|
46
|
+
debug('Toast persisted');
|
|
47
|
+
persisted = true;
|
|
48
|
+
toast.onclick = null;
|
|
49
|
+
toast.style.animation = 'none';
|
|
50
|
+
toast.style.opacity = '1';
|
|
51
|
+
|
|
52
|
+
const button = document.createElement('button');
|
|
53
|
+
button.classList.add('reset');
|
|
54
|
+
button.onclick = dismiss;
|
|
55
|
+
mount(Icon, { target: button, props: { i: 'xmark-large' } });
|
|
56
|
+
toast.appendChild(button);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (message && message instanceof Error) return persist();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @see https://ux.stackexchange.com/a/85898
|
|
63
|
+
*/
|
|
64
|
+
const duration = Math.min(Math.max(text.length * 50 * (durationMultiplier[type] || 1), 2000), 7000);
|
|
65
|
+
|
|
66
|
+
toast.onclick = persist;
|
|
67
|
+
await animate(toast, `fade-subtle ${duration}ms ease reverse forwards`);
|
|
68
|
+
if (!persisted) await dismiss();
|
|
69
|
+
else debug('Toast not auto-dismissed due to persistence');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function toastStatus(promise: Promise<unknown>, successMessage: string = text('generic.success')): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await promise;
|
|
75
|
+
await toast('success', successMessage);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
await toast('error', err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Object.assign(globalThis, { toast, toastStatus });
|
package/locales/en.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"AccessControlDialog": {
|
|
4
4
|
"named_title": "Permissions for <strong>{name}</strong>",
|
|
5
5
|
"owner": "Owner",
|
|
6
|
-
"title": "Permissions"
|
|
6
|
+
"title": "Permissions",
|
|
7
|
+
"remove": "Remove",
|
|
8
|
+
"toast_removed": "Removed access"
|
|
7
9
|
},
|
|
8
10
|
"AppMenu": {
|
|
9
11
|
"failed": "Couldn't load apps.",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"preferences": "Preferences",
|
|
73
75
|
"register": "Register",
|
|
74
76
|
"sessions": "Sessions",
|
|
77
|
+
"success": "Success",
|
|
75
78
|
"unknown": "Unknown",
|
|
76
79
|
"unnamed": "Unnamed",
|
|
77
80
|
"username": "Name",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"exports": {
|
|
34
34
|
"./package.json": "./package.json",
|
|
35
35
|
".": "./dist/index.js",
|
|
36
|
+
"./gui": "./dist/gui/index.js",
|
|
36
37
|
"./*": "./dist/*.js",
|
|
37
38
|
"./components": "./lib/index.js",
|
|
38
39
|
"./components/*": "./lib/*.svelte",
|
package/lib/Toast.svelte
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { fade } from 'svelte/transition';
|
|
3
|
-
|
|
4
|
-
const { enabled, children, delay = 5000, duration = 1000, ...rest } = $props();
|
|
5
|
-
|
|
6
|
-
let hiding = $state(false);
|
|
7
|
-
|
|
8
|
-
const show = $derived(enabled && !hiding);
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
{#if show}
|
|
12
|
-
<div
|
|
13
|
-
class="Toast"
|
|
14
|
-
in:fade|global={{ duration }}
|
|
15
|
-
onintroend={() => (hiding = true)}
|
|
16
|
-
out:fade|global={{ delay, duration }}
|
|
17
|
-
onoutroend={() => (hiding = false)}
|
|
18
|
-
{...rest}
|
|
19
|
-
>
|
|
20
|
-
{@render children()}
|
|
21
|
-
</div>
|
|
22
|
-
{/if}
|
|
23
|
-
|
|
24
|
-
<style>
|
|
25
|
-
.Toast {
|
|
26
|
-
position: fixed;
|
|
27
|
-
bottom: 1em;
|
|
28
|
-
left: calc(50% - 10em);
|
|
29
|
-
right: calc(50% - 10em);
|
|
30
|
-
width: 20em;
|
|
31
|
-
padding: 0.5em 1em;
|
|
32
|
-
border-radius: 1em;
|
|
33
|
-
opacity: 0.5;
|
|
34
|
-
}
|
|
35
|
-
</style>
|
|
File without changes
|
|
File without changes
|