@axium/client 0.17.2 → 0.18.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/animations.css +44 -5
- package/assets/styles.css +51 -11
- 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 +10 -7
- 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 +11 -8
- 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
|
@@ -120,7 +120,7 @@ button {
|
|
|
120
120
|
cursor: pointer;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
button:hover {
|
|
123
|
+
button:not(.reset):hover {
|
|
124
124
|
background-color: hsl(var(--hue) 15 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -144,7 +144,7 @@ dialog {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
dialog::backdrop {
|
|
147
|
+
dialog:modal::backdrop {
|
|
148
148
|
background: #0003;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -178,24 +178,33 @@ progress::-moz-progress-bar {
|
|
|
178
178
|
background-color: var(--bg-accent);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
:
|
|
181
|
+
:is(p, span, i).error {
|
|
182
|
+
color: var(--fg-error);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
input.error {
|
|
186
|
+
border: var(--border-error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
div.error {
|
|
182
190
|
padding: 1em;
|
|
183
191
|
border-radius: 0.5em;
|
|
184
192
|
background-color: var(--bg-error);
|
|
193
|
+
border: var(--border-error);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
border: 1px solid var(--bg-error);
|
|
196
|
+
div.warning {
|
|
197
|
+
padding: 1em;
|
|
198
|
+
border-radius: 0.5em;
|
|
199
|
+
background-color: var(--bg-warning);
|
|
200
|
+
border: var(--border-warning);
|
|
193
201
|
}
|
|
194
202
|
|
|
195
|
-
.success {
|
|
203
|
+
div.success {
|
|
196
204
|
padding: 1em;
|
|
197
205
|
border-radius: 0.5em;
|
|
198
206
|
background-color: var(--bg-success);
|
|
207
|
+
border: var(--border-success);
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
.subtle {
|
|
@@ -215,6 +224,7 @@ input.error {
|
|
|
215
224
|
border: var(--border-error);
|
|
216
225
|
background-color: hsl(0 20 calc(var(--bg-light) + var(--light-step)));
|
|
217
226
|
color: hsl(0 33 var(--fg-light));
|
|
227
|
+
--fill: hsl(0 33 var(--fg-light));
|
|
218
228
|
accent-color: hsl(0 33 var(--fg-light));
|
|
219
229
|
}
|
|
220
230
|
|
|
@@ -308,6 +318,36 @@ h6 {
|
|
|
308
318
|
}
|
|
309
319
|
}
|
|
310
320
|
|
|
321
|
+
#toasts {
|
|
322
|
+
position: absolute;
|
|
323
|
+
right: 1em;
|
|
324
|
+
top: 1em;
|
|
325
|
+
width: 0;
|
|
326
|
+
height: 0;
|
|
327
|
+
overflow: visible;
|
|
328
|
+
display: flex;
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
gap: 1em;
|
|
331
|
+
text-align: right;
|
|
332
|
+
align-items: end;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
div.toast {
|
|
336
|
+
width: max-content;
|
|
337
|
+
padding: 0.4em 0.8em;
|
|
338
|
+
border-radius: 0.5em;
|
|
339
|
+
|
|
340
|
+
&:hover {
|
|
341
|
+
animation-play-state: paused;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
&.plain,
|
|
345
|
+
&.info {
|
|
346
|
+
border: var(--border-accent);
|
|
347
|
+
background-color: var(--bg-accent);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
311
351
|
@media (width >= 700px) {
|
|
312
352
|
.mobile-only {
|
|
313
353
|
display: none;
|
|
@@ -333,7 +373,7 @@ h6 {
|
|
|
333
373
|
}
|
|
334
374
|
|
|
335
375
|
.mobile-button:hover {
|
|
336
|
-
background-color:
|
|
376
|
+
background-color: var(--bg-accent);
|
|
337
377
|
}
|
|
338
378
|
|
|
339
379
|
.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";
|
|
@@ -118,6 +121,13 @@ declare let currentLoaded: {
|
|
|
118
121
|
};
|
|
119
122
|
readonly admin: {
|
|
120
123
|
readonly heading: "Administration";
|
|
124
|
+
readonly tab: {
|
|
125
|
+
readonly dashboard: "Dashboard";
|
|
126
|
+
readonly users: "Users";
|
|
127
|
+
readonly config: "Configuration";
|
|
128
|
+
readonly plugins: "Plugins";
|
|
129
|
+
readonly audit: "Audit Log";
|
|
130
|
+
};
|
|
121
131
|
readonly audit: {
|
|
122
132
|
readonly any: "Any";
|
|
123
133
|
readonly apply: "Apply";
|
|
@@ -134,13 +144,6 @@ declare let currentLoaded: {
|
|
|
134
144
|
readonly until: "Until:";
|
|
135
145
|
readonly user: "User UUID:";
|
|
136
146
|
};
|
|
137
|
-
readonly tab: {
|
|
138
|
-
readonly dashboard: "Dashboard";
|
|
139
|
-
readonly users: "Users";
|
|
140
|
-
readonly config: "Configuration";
|
|
141
|
-
readonly plugins: "Plugins";
|
|
142
|
-
readonly audit: "Audit Log";
|
|
143
|
-
};
|
|
144
147
|
readonly filters: "Filters";
|
|
145
148
|
readonly heading: "Audit Log";
|
|
146
149
|
readonly invalid_filter: "Invalid Filter:";
|
|
@@ -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",
|
|
@@ -112,6 +115,13 @@
|
|
|
112
115
|
},
|
|
113
116
|
"admin": {
|
|
114
117
|
"heading": "Administration",
|
|
118
|
+
"tab": {
|
|
119
|
+
"dashboard": "Dashboard",
|
|
120
|
+
"users": "Users",
|
|
121
|
+
"config": "Configuration",
|
|
122
|
+
"plugins": "Plugins",
|
|
123
|
+
"audit": "Audit Log"
|
|
124
|
+
},
|
|
115
125
|
"audit": {
|
|
116
126
|
"any": "Any",
|
|
117
127
|
"apply": "Apply",
|
|
@@ -128,13 +138,6 @@
|
|
|
128
138
|
"until": "Until:",
|
|
129
139
|
"user": "User UUID:"
|
|
130
140
|
},
|
|
131
|
-
"tab": {
|
|
132
|
-
"dashboard": "Dashboard",
|
|
133
|
-
"users": "Users",
|
|
134
|
-
"config": "Configuration",
|
|
135
|
-
"plugins": "Plugins",
|
|
136
|
-
"audit": "Audit Log"
|
|
137
|
-
},
|
|
138
141
|
"filters": "Filters",
|
|
139
142
|
"heading": "Audit Log",
|
|
140
143
|
"invalid_filter": "Invalid Filter:",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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
|