@axium/client 0.33.5 → 0.34.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
@@ -441,6 +441,42 @@ div.toast {
441
441
  border: var(--border-accent);
442
442
  background-color: var(--bg-accent);
443
443
  }
444
+
445
+ &.progress {
446
+ border: var(--border-accent);
447
+ background-color: var(--bg-accent);
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 0.25em;
451
+ text-align: left;
452
+ min-width: 16em;
453
+
454
+ .toast-header {
455
+ display: flex;
456
+ align-items: center;
457
+ justify-content: space-between;
458
+ gap: 1em;
459
+
460
+ button {
461
+ display: inline-flex;
462
+ cursor: pointer;
463
+ }
464
+ }
465
+
466
+ .subtle {
467
+ opacity: 69%;
468
+ font-size: 0.85em;
469
+
470
+ &:empty {
471
+ display: none;
472
+ }
473
+ }
474
+
475
+ progress {
476
+ width: 100%;
477
+ height: 4px;
478
+ }
479
+ }
444
480
  }
445
481
 
446
482
  @media (width >= 700px) {
package/dist/locales.d.ts CHANGED
@@ -129,6 +129,7 @@ declare let currentLoaded: {
129
129
  readonly preferences: "Preferences";
130
130
  readonly sessions: "Sessions";
131
131
  readonly title: "Your Account";
132
+ readonly toast_username_updated: "Username updated";
132
133
  readonly user_id: "User ID";
133
134
  readonly user_id_hint: "This is your UUID. It can't be changed.";
134
135
  readonly verification_sent: "Verification email sent";
@@ -39,7 +39,7 @@
39
39
  </div>
40
40
 
41
41
  {#each acl as control, i (control.userId || control.role || control.tag)}
42
- {@const update = (key: string) => async (e: Event & { currentTarget: HTMLInputElement }) => {
42
+ {const update = (key: string) => async (e: Event & { currentTarget: HTMLInputElement }) => {
43
43
  try {
44
44
  const updated = await updateACL(itemType, item.id, getTarget(control), { [key]: e.currentTarget.checked });
45
45
  Object.assign(control, updated);
@@ -80,7 +80,7 @@
80
80
  </div>
81
81
  <div class="permissions">
82
82
  {#each Object.entries(pickPermissions(control) as Record<string, boolean>) as [key, value]}
83
- {@const id = `${item.id}.${getTarget(control)}.${key}`}
83
+ {const id = $derived(`${item.id}.${getTarget(control)}.${key}`)}
84
84
  <span class="icon-text">
85
85
  {#if editable}
86
86
  <input {id} type="checkbox" onchange={update(key)} />
@@ -24,7 +24,7 @@
24
24
  /** Change the text displayed for the submit button */
25
25
  submitText?: string;
26
26
  /** Called on submission, this should do the actual submission */
27
- submit?(data: Record<string, FormDataEntryValue>): Promise<any>;
27
+ submit?(data: Record<string, FormDataEntryValue>): any;
28
28
  /** Whether to display the dialog as a full-page form */
29
29
  pageMode?: boolean;
30
30
  submitDanger?: boolean;
@@ -47,7 +47,7 @@
47
47
  function onsubmit(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
48
48
  e.preventDefault();
49
49
  const data = Object.fromEntries(new FormData(e.currentTarget));
50
- submit(data)
50
+ Promise.resolve(submit(data))
51
51
  .then(result => {
52
52
  if (!pageMode) dialog!.close();
53
53
  })
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { prettifyError, type ZodType } from 'zod';
4
+ import Icon from './Icon.svelte';
5
+
6
+ let {
7
+ value,
8
+ commit,
9
+ close,
10
+ schema,
11
+ optional = false,
12
+ selectionRange,
13
+ placeholder,
14
+ confirmLabel = text('generic.change'),
15
+ }: {
16
+ /** The committed value being edited. */
17
+ value?: string | null;
18
+ /** Called with the new value when a change is confirmed. `null` means the value was cleared (only possible when `optional`). */
19
+ commit(value: string | null): unknown;
20
+ /** Called when editing ends, whether or not a change was committed. */
21
+ close(): void;
22
+ /** Validates the draft as the user types and before committing. Errors float above the input. */
23
+ schema?: ZodType;
24
+ /** Whether clearing the input commits `null` instead of closing without changes. */
25
+ optional?: boolean;
26
+ /** The range of the value to select when editing starts. Defaults to the entire value. */
27
+ selectionRange?: [number, number];
28
+ placeholder?: string;
29
+ confirmLabel?: string;
30
+ } = $props();
31
+
32
+ let draft = $state(value ?? ''),
33
+ error = $state<string>();
34
+
35
+ function validate(val: string | null): boolean {
36
+ const result = val === null && optional ? undefined : schema?.safeParse(val);
37
+ error = result?.error ? prettifyError(result.error) : undefined;
38
+ return !result || result.success;
39
+ }
40
+
41
+ function confirm() {
42
+ const newValue = draft.trim() || null;
43
+ if (newValue === (value ?? null) || (newValue === null && !optional)) return close();
44
+ if (!validate(newValue)) return;
45
+ commit(newValue);
46
+ close();
47
+ }
48
+ </script>
49
+
50
+ <span
51
+ class="InlineEdit"
52
+ data-no-select
53
+ onclick={e => e.stopPropagation()}
54
+ {@attach span => {
55
+ const input = span.querySelector('input')!;
56
+ requestAnimationFrame(() => {
57
+ input.focus();
58
+ input.setSelectionRange(...(selectionRange ?? [0, draft.length]));
59
+ });
60
+ const onOutside = (e: PointerEvent) => !span.contains(e.target as Node) && close();
61
+ requestAnimationFrame(() => document.addEventListener('pointerdown', onOutside, true));
62
+ return () => document.removeEventListener('pointerdown', onOutside, true);
63
+ }}
64
+ >
65
+ {#if error}<span class="InlineEdit-error">{error}</span>{/if}
66
+ <input
67
+ bind:value={draft}
68
+ {placeholder}
69
+ class={[error && 'error']}
70
+ oninput={() => validate(draft.trim() || null)}
71
+ onkeydown={e => {
72
+ if (e.key == 'Enter') confirm();
73
+ else if (e.key == 'Escape') close();
74
+ }}
75
+ />
76
+ <button class="reset InlineEdit-confirm" aria-label={confirmLabel} onclick={confirm}>
77
+ <Icon i="check" />
78
+ </button>
79
+ </span>
80
+
81
+ <style>
82
+ .InlineEdit {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 0.5em;
86
+ min-width: 0;
87
+ anchor-scope: --inline-edit;
88
+ }
89
+
90
+ input {
91
+ background: var(--bg-normal);
92
+ border: var(--border-accent);
93
+ border-radius: 0.25em;
94
+ padding: 0.1em 0.25em;
95
+ margin: -0.1em 0;
96
+ font: inherit;
97
+ color: inherit;
98
+ min-width: 0;
99
+ flex: 1 1 auto;
100
+ anchor-name: --inline-edit;
101
+ }
102
+
103
+ .InlineEdit-error {
104
+ position: fixed;
105
+ position-anchor: --inline-edit;
106
+ bottom: calc(anchor(top) - 0.3em);
107
+ left: anchor(left);
108
+ color: var(--fg-error);
109
+ }
110
+
111
+ .InlineEdit-confirm {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ flex: 0 0 auto;
115
+ cursor: pointer;
116
+ }
117
+ </style>
package/lib/Upload.svelte CHANGED
@@ -11,7 +11,8 @@
11
11
  ...rest
12
12
  }: HTMLInputAttributes & { input?: HTMLInputElement; progress?: [current: number, max: number][] } = $props();
13
13
 
14
- const id = $props.id();
14
+ const defaultId = $props.id();
15
+ const id = rest.id || defaultId;
15
16
  </script>
16
17
 
17
18
  <div>
package/lib/index.ts CHANGED
@@ -9,6 +9,7 @@ export { default as Discovery } from './Discovery.svelte';
9
9
  export * as discovery from './Discovery.svelte';
10
10
  export { default as FormDialog } from './FormDialog.svelte';
11
11
  export { default as Icon } from './Icon.svelte';
12
+ export { default as InlineEdit } from './InlineEdit.svelte';
12
13
  export { default as LocationSelect } from './LocationSelect.svelte';
13
14
  export { default as Login } from './Login.svelte';
14
15
  export { default as Logout } from './Logout.svelte';
package/lib/toast.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { debug, errorText } from 'ioium';
1
+ import { debug, errorText, useProgress } from 'ioium';
2
2
  import { text } from '@axium/client/locales';
3
3
  import { animate, onAnimationEnd } from '@axium/client/gui';
4
4
  import Icon from './Icon.svelte';
@@ -83,4 +83,74 @@ export async function toastStatus(promise: Promise<unknown>, successMessage: str
83
83
  }
84
84
  }
85
85
 
86
+ let progressToast: HTMLDivElement | undefined,
87
+ progressMain: HTMLSpanElement,
88
+ progressMessage: HTMLSpanElement,
89
+ progressBar: HTMLProgressElement,
90
+ progressCancelButton: HTMLButtonElement,
91
+ progressCancel: (() => void) | undefined;
92
+
93
+ function warnBeforeUnload(e: BeforeUnloadEvent) {
94
+ e.preventDefault();
95
+ }
96
+
97
+ /**
98
+ * Register a callback used to cancel the operation shown in the progress toast.
99
+ * The toast's cancel button is only shown while a callback is registered.
100
+ */
101
+ export function setProgressCancel(cancel?: () => void): void {
102
+ progressCancel = cancel;
103
+ if (progressToast) progressCancelButton.style.display = cancel ? '' : 'none';
104
+ }
105
+
106
+ useProgress({
107
+ start(message: string): void {
108
+ if (!progressToast) {
109
+ progressToast = document.createElement('div');
110
+ progressToast.classList.add('toast', 'progress');
111
+
112
+ const header = document.createElement('div');
113
+ header.classList.add('toast-header');
114
+ progressToast.appendChild(header);
115
+
116
+ progressMain = document.createElement('span');
117
+ header.appendChild(progressMain);
118
+
119
+ progressCancelButton = document.createElement('button');
120
+ progressCancelButton.classList.add('reset');
121
+ progressCancelButton.onclick = () => progressCancel?.();
122
+ mount(Icon, { target: progressCancelButton, props: { i: 'xmark-large' } });
123
+ header.appendChild(progressCancelButton);
124
+
125
+ progressMessage = document.createElement('span');
126
+ progressMessage.classList.add('subtle');
127
+ progressToast.appendChild(progressMessage);
128
+
129
+ progressBar = document.createElement('progress');
130
+ progressToast.appendChild(progressBar);
131
+
132
+ list.appendChild(progressToast);
133
+ addEventListener('beforeunload', warnBeforeUnload);
134
+ }
135
+ progressMain.textContent = message;
136
+ progressMessage.textContent = '';
137
+ progressCancelButton.style.display = progressCancel ? '' : 'none';
138
+ progressBar.removeAttribute('value');
139
+ progressBar.max = 1;
140
+ },
141
+ progress(value: number, max: number, message?: any): void {
142
+ if (!progressToast) return;
143
+ progressBar.value = value;
144
+ progressBar.max = max;
145
+ progressMessage.textContent = message == undefined ? '' : String(message);
146
+ },
147
+ done(): void {
148
+ if (!progressToast) return;
149
+ progressToast.remove();
150
+ progressToast = undefined;
151
+ progressCancel = undefined;
152
+ removeEventListener('beforeunload', warnBeforeUnload);
153
+ },
154
+ });
155
+
86
156
  Object.assign(globalThis, { toast, toastStatus });
package/locales/en.json CHANGED
@@ -123,6 +123,7 @@
123
123
  "preferences": "Preferences",
124
124
  "sessions": "Sessions",
125
125
  "title": "Your Account",
126
+ "toast_username_updated": "Username updated",
126
127
  "user_id": "User ID",
127
128
  "user_id_hint": "This is your UUID. It can't be changed.",
128
129
  "verification_sent": "Verification email sent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.33.5",
3
+ "version": "0.34.0",
4
4
  "author": "James Prevett <jp@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -41,7 +41,7 @@
41
41
  margin-left: 1em;
42
42
  }
43
43
 
44
- > :nth-child(2) {
44
+ > :nth-child(2):not(.InlineEdit) {
45
45
  text-overflow: ellipsis;
46
46
  overflow: hidden;
47
47
  }