@svadmin/ui 0.86.0 → 0.88.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/README.md +3 -1
- package/dist/app.css +188 -5
- package/dist/app.theme.css +188 -5
- package/dist/component-registry.d.ts +1 -1
- package/dist/component-registry.js +2 -2
- package/dist/components/ApprovalActionCard.svelte +54 -31
- package/dist/components/ApprovalCenter.svelte +73 -31
- package/dist/components/AuthPageShell.svelte +13 -2
- package/dist/components/AutoTable.svelte +13 -3
- package/dist/components/CreatePage.svelte +22 -15
- package/dist/components/CreatePage.svelte.d.ts +4 -0
- package/dist/components/EditPage.svelte +23 -16
- package/dist/components/EditPage.svelte.d.ts +4 -0
- package/dist/components/ListPage.svelte +20 -14
- package/dist/components/ListPage.svelte.d.ts +2 -0
- package/dist/components/LoginPage.svelte +27 -11
- package/dist/components/LoginPage.svelte.d.ts +4 -0
- package/dist/components/NotificationsSettings.svelte +16 -8
- package/dist/components/PagePattern.svelte +2 -2
- package/dist/components/PagePattern.svelte.d.ts +1 -1
- package/dist/components/PasswordInput.svelte +36 -17
- package/dist/components/PasswordInput.svelte.d.ts +3 -0
- package/dist/components/ShowPage.svelte +20 -21
- package/dist/components/account/MemberDirectory.svelte +17 -4
- package/dist/components/account/SecurityLogPage.svelte +8 -1
- package/dist/components/content/AuditTimeline.svelte +32 -4
- package/dist/components/content/AuditTimeline.svelte.d.ts +9 -0
- package/dist/components/content/DataState.svelte +5 -4
- package/dist/components/content/DataState.svelte.d.ts +1 -1
- package/dist/components/content/FilterToolbar.svelte +7 -1
- package/dist/components/content/WorkspaceLayout.svelte +3 -1
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +1 -0
- package/dist/components/network/TeamCrewTablePage.svelte +8 -1
- package/dist/components/network/UserCardsNFTPage.svelte +3 -1
- package/dist/components/profile/ProjectsGrid.svelte +1 -1
- package/dist/components/profile/TeamsShowcase.svelte +1 -1
- package/dist/design-contract.d.ts +1 -1
- package/dist/design-contract.js +1 -1
- package/dist/page-chrome.css +3 -3
- package/dist/recipes.d.ts +102 -69
- package/dist/recipes.js +40 -2
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import PageHeader from './PageHeader.svelte';
|
|
10
10
|
import AutoTable from './AutoTable.svelte';
|
|
11
11
|
import type { BatchSelection } from './table-contract';
|
|
12
|
+
import type { SavedListViewProvider } from './saved-list-views.js';
|
|
12
13
|
import CreateButton from './buttons/CreateButton.svelte';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
/** Passthrough: enable row selection checkboxes */
|
|
20
21
|
selectable?: boolean;
|
|
21
22
|
allowSelectAllMatching?: boolean;
|
|
23
|
+
savedViewProvider?: SavedListViewProvider;
|
|
22
24
|
/** Passthrough: custom header actions */
|
|
23
25
|
headerActions?: Snippet;
|
|
24
26
|
/** Optional status tabs slot above filter/table */
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
canCreate,
|
|
46
48
|
selectable,
|
|
47
49
|
allowSelectAllMatching,
|
|
50
|
+
savedViewProvider,
|
|
48
51
|
headerActions,
|
|
49
52
|
statusTabs,
|
|
50
53
|
filterToolbar,
|
|
@@ -60,7 +63,7 @@
|
|
|
60
63
|
const showCreate = $derived(canCreate ?? resource.canCreate !== false);
|
|
61
64
|
</script>
|
|
62
65
|
|
|
63
|
-
<div class="svadmin-u-3e7ce58d64fa {className}">
|
|
66
|
+
<div class="svadmin-u-3e7ce58d64fa {className}" data-svadmin-list-page={resourceName}>
|
|
64
67
|
<PageHeader title={pageTitle}>
|
|
65
68
|
{#snippet actions()}
|
|
66
69
|
{#if showCreate}
|
|
@@ -73,26 +76,29 @@
|
|
|
73
76
|
</PageHeader>
|
|
74
77
|
|
|
75
78
|
{#if statusTabs}
|
|
76
|
-
<div>
|
|
79
|
+
<div data-svadmin-list-page-status-tabs>
|
|
77
80
|
{@render statusTabs()}
|
|
78
81
|
</div>
|
|
79
82
|
{/if}
|
|
80
83
|
|
|
81
84
|
{#if filterToolbar}
|
|
82
|
-
<div>
|
|
85
|
+
<div data-svadmin-list-page-filter-toolbar>
|
|
83
86
|
{@render filterToolbar()}
|
|
84
87
|
</div>
|
|
85
88
|
{/if}
|
|
86
89
|
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
<div data-svadmin-list-page-table>
|
|
91
|
+
<AutoTable
|
|
92
|
+
{rendering}
|
|
93
|
+
{resourceName}
|
|
94
|
+
showHeader={false}
|
|
95
|
+
{...definedOptions({ "selectable": selectable })}
|
|
96
|
+
{...definedOptions({ allowSelectAllMatching })}
|
|
97
|
+
{...definedOptions({ savedViewProvider })}
|
|
98
|
+
{...definedOptions({ batchActions })}
|
|
99
|
+
{...definedOptions({ "defaultCellRenderer": cellRenderer })}
|
|
100
|
+
{...definedOptions({ "rowActions": rowActions })}
|
|
101
|
+
{...definedOptions({ emptyState, expandedRowRender })}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
98
104
|
</div>
|
|
@@ -2,6 +2,7 @@ import type { ResourceRendering } from '../rendering/index.js';
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { FieldDefinition } from '@svadmin/core';
|
|
4
4
|
import type { BatchSelection } from './table-contract';
|
|
5
|
+
import type { SavedListViewProvider } from './saved-list-views.js';
|
|
5
6
|
interface Props {
|
|
6
7
|
resourceName: string;
|
|
7
8
|
rendering?: ResourceRendering | undefined;
|
|
@@ -10,6 +11,7 @@ interface Props {
|
|
|
10
11
|
/** Passthrough: enable row selection checkboxes */
|
|
11
12
|
selectable?: boolean;
|
|
12
13
|
allowSelectAllMatching?: boolean;
|
|
14
|
+
savedViewProvider?: SavedListViewProvider;
|
|
13
15
|
/** Passthrough: custom header actions */
|
|
14
16
|
headerActions?: Snippet;
|
|
15
17
|
/** Optional status tabs slot above filter/table */
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
defaultIdentifier = '',
|
|
26
26
|
defaultPassword = '',
|
|
27
27
|
loginHint,
|
|
28
|
+
identifierLabel,
|
|
29
|
+
identifierPlaceholder,
|
|
30
|
+
passwordLabel,
|
|
31
|
+
loginButtonLabel,
|
|
28
32
|
} = $props<{
|
|
29
33
|
title?: string;
|
|
30
34
|
onSuccess?: () => void;
|
|
@@ -32,6 +36,10 @@
|
|
|
32
36
|
defaultIdentifier?: string;
|
|
33
37
|
defaultPassword?: string;
|
|
34
38
|
loginHint?: string;
|
|
39
|
+
identifierLabel?: string;
|
|
40
|
+
identifierPlaceholder?: string;
|
|
41
|
+
passwordLabel?: string;
|
|
42
|
+
loginButtonLabel?: string;
|
|
35
43
|
}>();
|
|
36
44
|
|
|
37
45
|
const adminContext = captureAdminContext();
|
|
@@ -72,6 +80,7 @@
|
|
|
72
80
|
{/if}
|
|
73
81
|
{/snippet}
|
|
74
82
|
|
|
83
|
+
<div data-svadmin-login-page>
|
|
75
84
|
<AuthPageShell brand={title} title={i18n.t('auth.welcomeBack')} description={i18n.t('auth.welcomeMessage')} {footer}>
|
|
76
85
|
{#if loginHint || defaultIdentifier || defaultPassword}
|
|
77
86
|
<aside data-svadmin-demo-credentials class="svadmin-u-fb88ccaacf5b svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-60ed9c3ef326 svadmin-u-eb6e8b881acd">
|
|
@@ -95,9 +104,9 @@
|
|
|
95
104
|
</aside>
|
|
96
105
|
{/if}
|
|
97
106
|
|
|
98
|
-
<form onsubmit={handleSubmit} class="svadmin-u-b43b4c086d9a">
|
|
107
|
+
<form onsubmit={handleSubmit} class="svadmin-u-b43b4c086d9a" data-svadmin-login-form>
|
|
99
108
|
{#if error}
|
|
100
|
-
<Alert.Root variant="destructive">
|
|
109
|
+
<Alert.Root variant="destructive" id="login-error" data-svadmin-login-error aria-live="assertive">
|
|
101
110
|
<AlertCircle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
102
111
|
<Alert.Description class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069">{error}</Alert.Description>
|
|
103
112
|
</Alert.Root>
|
|
@@ -105,17 +114,20 @@
|
|
|
105
114
|
|
|
106
115
|
<div class="svadmin-u-6f7e013d6499">
|
|
107
116
|
<Label for="login-identifier">
|
|
108
|
-
{i18n.t('auth.usernameOrEmail')}
|
|
117
|
+
{identifierLabel ?? i18n.t('auth.usernameOrEmail')}
|
|
109
118
|
</Label>
|
|
110
|
-
<div class="svadmin-
|
|
111
|
-
<User class="svadmin-
|
|
119
|
+
<div class="svadmin-auth-input-field" data-svadmin-login-identifier-field>
|
|
120
|
+
<User class="svadmin-auth-input-icon" data-svadmin-auth-input-icon aria-hidden="true" />
|
|
112
121
|
<Input
|
|
113
122
|
id="login-identifier"
|
|
123
|
+
data-svadmin-login-identifier
|
|
114
124
|
type="text"
|
|
115
|
-
placeholder={i18n.t('auth.identifierPlaceholder')}
|
|
125
|
+
placeholder={identifierPlaceholder ?? i18n.t('auth.identifierPlaceholder')}
|
|
116
126
|
bind:value={identifier}
|
|
117
|
-
class="svadmin-
|
|
127
|
+
class="svadmin-auth-input-control"
|
|
118
128
|
autocomplete="username"
|
|
129
|
+
aria-invalid={error ? 'true' : undefined}
|
|
130
|
+
aria-describedby={error ? 'login-error' : undefined}
|
|
119
131
|
/>
|
|
120
132
|
</div>
|
|
121
133
|
</div>
|
|
@@ -123,7 +135,7 @@
|
|
|
123
135
|
<div class="svadmin-u-6f7e013d6499">
|
|
124
136
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc">
|
|
125
137
|
<Label for="login-password">
|
|
126
|
-
{i18n.t('auth.password')}
|
|
138
|
+
{passwordLabel ?? i18n.t('auth.password')}
|
|
127
139
|
</Label>
|
|
128
140
|
{#if authProvider?.forgotPassword}
|
|
129
141
|
<Button variant="link" class="svadmin-u-b8f0a08ece1e svadmin-u-8a539c7fe216 svadmin-u-359090c2d529 svadmin-u-2689f3958069" onclick={() => adminContext.navigate('/forgot-password')}>
|
|
@@ -133,17 +145,20 @@
|
|
|
133
145
|
</div>
|
|
134
146
|
<PasswordInput
|
|
135
147
|
id="login-password"
|
|
136
|
-
label=
|
|
148
|
+
label={passwordLabel ?? i18n.t('auth.password')}
|
|
149
|
+
showLabel={false}
|
|
137
150
|
bind:value={password}
|
|
138
151
|
autocomplete="current-password"
|
|
152
|
+
ariaInvalid={error ? 'true' : undefined}
|
|
153
|
+
ariaDescribedby={error ? 'login-error' : undefined}
|
|
139
154
|
/>
|
|
140
155
|
</div>
|
|
141
156
|
|
|
142
|
-
<Button type="submit" class="svadmin-u-426b8b75185b svadmin-u-6da6a3c3f741" disabled={login.isLoading}>
|
|
157
|
+
<Button type="submit" data-svadmin-login-submit class="svadmin-u-426b8b75185b svadmin-u-6da6a3c3f741" disabled={login.isLoading} aria-busy={login.isLoading}>
|
|
143
158
|
{#if login.isLoading}
|
|
144
159
|
<Loader2 class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-afbdd13a380e svadmin-u-d2347e8497a9" />
|
|
145
160
|
{/if}
|
|
146
|
-
{i18n.t('auth.loginButton')}
|
|
161
|
+
{loginButtonLabel ?? i18n.t('auth.loginButton')}
|
|
147
162
|
</Button>
|
|
148
163
|
</form>
|
|
149
164
|
|
|
@@ -171,3 +186,4 @@
|
|
|
171
186
|
</div>
|
|
172
187
|
{/if}
|
|
173
188
|
</AuthPageShell>
|
|
189
|
+
</div>
|
|
@@ -10,6 +10,10 @@ type $$ComponentProps = {
|
|
|
10
10
|
defaultIdentifier?: string;
|
|
11
11
|
defaultPassword?: string;
|
|
12
12
|
loginHint?: string;
|
|
13
|
+
identifierLabel?: string;
|
|
14
|
+
identifierPlaceholder?: string;
|
|
15
|
+
passwordLabel?: string;
|
|
16
|
+
loginButtonLabel?: string;
|
|
13
17
|
};
|
|
14
18
|
declare const LoginPage: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
19
|
type LoginPage = ReturnType<typeof LoginPage>;
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
let loading = $state(false);
|
|
34
34
|
let saving = $state(false);
|
|
35
35
|
let loaded = $state(false);
|
|
36
|
-
let
|
|
36
|
+
let loadError = $state('');
|
|
37
|
+
let saveError = $state('');
|
|
37
38
|
let epoch = 0;
|
|
38
39
|
async function load() {
|
|
39
40
|
const provider = preferencesProvider;
|
|
40
41
|
const request = ++epoch;
|
|
41
|
-
|
|
42
|
+
loadError = ''; saveError = ''; loaded = false; saving = false;
|
|
42
43
|
loading = Boolean(provider);
|
|
43
44
|
if (!provider) return;
|
|
44
45
|
try {
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
emailAlerts = { ...result.email }; pushAlerts = { ...result.push }; smsAlerts = { ...result.sms };
|
|
48
49
|
loaded = true;
|
|
49
50
|
} catch (caught) {
|
|
50
|
-
if (request === epoch)
|
|
51
|
+
if (request === epoch) loadError = caught instanceof Error ? caught.message : String(caught);
|
|
51
52
|
} finally {
|
|
52
53
|
if (request === epoch) loading = false;
|
|
53
54
|
}
|
|
@@ -61,12 +62,12 @@
|
|
|
61
62
|
const provider = preferencesProvider;
|
|
62
63
|
if (!provider || !loaded || saving) return;
|
|
63
64
|
const request = epoch;
|
|
64
|
-
saving = true;
|
|
65
|
+
saving = true; saveError = '';
|
|
65
66
|
try {
|
|
66
67
|
await provider.save({ email: { ...emailAlerts }, push: { ...pushAlerts }, sms: { ...smsAlerts } });
|
|
67
68
|
if (request === epoch) notification.success(i18n.t('common.updateSuccess'), 3000);
|
|
68
69
|
} catch (caught) {
|
|
69
|
-
if (request === epoch)
|
|
70
|
+
if (request === epoch) saveError = caught instanceof Error ? caught.message : String(caught);
|
|
70
71
|
} finally {
|
|
71
72
|
if (request === epoch) saving = false;
|
|
72
73
|
}
|
|
@@ -85,9 +86,15 @@
|
|
|
85
86
|
{#if !preferencesProvider}
|
|
86
87
|
<FeedbackNotice tone="warning" message={i18n.locale === 'zh-CN' ? '未配置通知偏好存储服务,设置不可保存。' : 'Notification preference storage is not configured. Settings cannot be saved.'} />
|
|
87
88
|
{/if}
|
|
88
|
-
{#if loading}
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
{#if loading}
|
|
90
|
+
<DataState state="loading" />
|
|
91
|
+
{:else if loadError}
|
|
92
|
+
<DataState state="error" description={loadError} retry={load} />
|
|
93
|
+
{:else if saveError}
|
|
94
|
+
<FeedbackNotice tone="danger" message={saveError} />
|
|
95
|
+
{/if}
|
|
96
|
+
{#if loaded || !preferencesProvider}
|
|
97
|
+
<fieldset disabled={!preferencesProvider || saving} class="svadmin-u-b3542e058833">
|
|
91
98
|
<SettingsGroup title={i18n.t('notifications.emailTitle')} description={i18n.t('notifications.emailDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
92
99
|
<div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3">
|
|
93
100
|
<SettingsFieldRow controlId={`${controlId}-email-security`} label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityAlertsDesc')}>{#snippet control()}<Switch id={`${controlId}-email-security`} bind:checked={emailAlerts.security} />{/snippet}</SettingsFieldRow>
|
|
@@ -106,4 +113,5 @@
|
|
|
106
113
|
<SettingsFieldRow controlId={`${controlId}-sms-security`} label={i18n.t('notifications.criticalSecurity')} description={i18n.t('notifications.criticalSecurityDesc')}>{#snippet control()}<Switch id={`${controlId}-sms-security`} bind:checked={smsAlerts.security} />{/snippet}</SettingsFieldRow>
|
|
107
114
|
</SettingsGroup>
|
|
108
115
|
</fieldset>
|
|
116
|
+
{/if}
|
|
109
117
|
</div>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import DataState from './content/DataState.svelte';
|
|
7
7
|
|
|
8
8
|
export type PagePatternKind = 'list' | 'form' | 'detail' | 'workspace' | 'dashboard';
|
|
9
|
-
export type PagePatternState = 'ready' | 'loading' | 'empty' | 'error' | 'forbidden';
|
|
9
|
+
export type PagePatternState = 'ready' | 'loading' | 'empty' | 'filtered-empty' | 'error' | 'forbidden';
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
kind: PagePatternKind;
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
>
|
|
69
69
|
<div data-page-pattern-kind={kind} data-page-pattern-state={state}>
|
|
70
70
|
{#if showState}
|
|
71
|
-
{#if state === 'loading' || state === 'empty' || state === 'error' || state === 'forbidden'}
|
|
71
|
+
{#if state === 'loading' || state === 'empty' || state === 'filtered-empty' || state === 'error' || state === 'forbidden'}
|
|
72
72
|
<DataState
|
|
73
73
|
{state}
|
|
74
74
|
{...definedOptions({ title: stateTitle })}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
export type PagePatternKind = 'list' | 'form' | 'detail' | 'workspace' | 'dashboard';
|
|
3
|
-
export type PagePatternState = 'ready' | 'loading' | 'empty' | 'error' | 'forbidden';
|
|
3
|
+
export type PagePatternState = 'ready' | 'loading' | 'empty' | 'filtered-empty' | 'error' | 'forbidden';
|
|
4
4
|
interface Props {
|
|
5
5
|
kind: PagePatternKind;
|
|
6
6
|
title: string;
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
autocomplete?: FullAutoFill;
|
|
17
17
|
showStrength?: boolean;
|
|
18
18
|
disabled?: boolean;
|
|
19
|
+
showLabel?: boolean;
|
|
19
20
|
class?: string;
|
|
21
|
+
ariaInvalid?: 'true' | 'false' | undefined;
|
|
22
|
+
ariaDescribedby?: string | undefined;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
let {
|
|
@@ -27,7 +30,10 @@
|
|
|
27
30
|
autocomplete = 'current-password',
|
|
28
31
|
showStrength = false,
|
|
29
32
|
disabled = false,
|
|
33
|
+
showLabel = true,
|
|
30
34
|
class: className = '',
|
|
35
|
+
ariaInvalid,
|
|
36
|
+
ariaDescribedby,
|
|
31
37
|
}: Props = $props();
|
|
32
38
|
|
|
33
39
|
let showPassword = $state(false);
|
|
@@ -44,10 +50,10 @@
|
|
|
44
50
|
return Math.min(score, 100);
|
|
45
51
|
});
|
|
46
52
|
|
|
47
|
-
const
|
|
48
|
-
strength < 30 ? '
|
|
49
|
-
strength < 60 ? '
|
|
50
|
-
strength < 80 ? '
|
|
53
|
+
const strengthTone = $derived(
|
|
54
|
+
strength < 30 ? 'danger' :
|
|
55
|
+
strength < 60 ? 'warning' :
|
|
56
|
+
strength < 80 ? 'info' : 'success'
|
|
51
57
|
);
|
|
52
58
|
|
|
53
59
|
const strengthLabel = $derived(
|
|
@@ -57,45 +63,58 @@
|
|
|
57
63
|
);
|
|
58
64
|
</script>
|
|
59
65
|
|
|
60
|
-
<div
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
<div
|
|
67
|
+
class="svadmin-password-input {className}"
|
|
68
|
+
data-svadmin-password-input
|
|
69
|
+
data-svadmin-login-password
|
|
70
|
+
data-password-visible={showPassword ? 'true' : 'false'}
|
|
71
|
+
>
|
|
72
|
+
{#if showLabel}
|
|
73
|
+
<Label for={id} class="svadmin-password-input__label" data-svadmin-password-label>{label}</Label>
|
|
74
|
+
{/if}
|
|
75
|
+
<div class="svadmin-password-input__field" data-svadmin-password-field>
|
|
76
|
+
<Lock class="svadmin-password-input__icon" data-svadmin-password-icon aria-hidden="true" />
|
|
64
77
|
<Input
|
|
65
78
|
{id}
|
|
66
79
|
type={showPassword ? 'text' : 'password'}
|
|
67
80
|
{placeholder}
|
|
68
81
|
bind:value
|
|
69
|
-
class="svadmin-
|
|
82
|
+
class="svadmin-password-input__control"
|
|
83
|
+
data-svadmin-password-control
|
|
70
84
|
{autocomplete}
|
|
71
85
|
{disabled}
|
|
86
|
+
aria-invalid={ariaInvalid}
|
|
87
|
+
aria-describedby={ariaDescribedby}
|
|
72
88
|
/>
|
|
73
89
|
<TooltipButton
|
|
74
90
|
tooltip={showPassword ? i18n.t('common.hidePassword') : i18n.t('common.showPassword')}
|
|
75
91
|
variant="ghost"
|
|
76
92
|
size="icon"
|
|
77
93
|
type="button"
|
|
78
|
-
class="svadmin-
|
|
94
|
+
class="svadmin-password-input__toggle"
|
|
95
|
+
data-svadmin-password-toggle
|
|
96
|
+
aria-controls={id}
|
|
97
|
+
aria-pressed={showPassword}
|
|
79
98
|
onclick={(e) => { e.preventDefault(); showPassword = !showPassword; }}
|
|
80
99
|
{disabled}
|
|
81
|
-
tabindex={-1}
|
|
82
100
|
>
|
|
83
101
|
{#if showPassword}
|
|
84
|
-
<EyeOff class="svadmin-
|
|
102
|
+
<EyeOff class="svadmin-password-input__glyph" />
|
|
85
103
|
{:else}
|
|
86
|
-
<Eye class="svadmin-
|
|
104
|
+
<Eye class="svadmin-password-input__glyph" />
|
|
87
105
|
{/if}
|
|
88
106
|
</TooltipButton>
|
|
89
107
|
</div>
|
|
90
108
|
{#if showStrength && value}
|
|
91
|
-
<div class="svadmin-
|
|
92
|
-
<div class="svadmin-
|
|
109
|
+
<div class="svadmin-password-input__strength" data-svadmin-password-strength>
|
|
110
|
+
<div class="svadmin-password-input__strength-track">
|
|
93
111
|
<div
|
|
94
|
-
class="svadmin-
|
|
112
|
+
class="svadmin-password-input__strength-fill"
|
|
113
|
+
data-tone={strengthTone}
|
|
95
114
|
style="width: {strength}%"
|
|
96
115
|
></div>
|
|
97
116
|
</div>
|
|
98
|
-
<span class="svadmin-
|
|
117
|
+
<span class="svadmin-password-input__strength-label">{strengthLabel}</span>
|
|
99
118
|
</div>
|
|
100
119
|
{/if}
|
|
101
120
|
</div>
|
|
@@ -7,7 +7,10 @@ interface Props {
|
|
|
7
7
|
autocomplete?: FullAutoFill;
|
|
8
8
|
showStrength?: boolean;
|
|
9
9
|
disabled?: boolean;
|
|
10
|
+
showLabel?: boolean;
|
|
10
11
|
class?: string;
|
|
12
|
+
ariaInvalid?: 'true' | 'false' | undefined;
|
|
13
|
+
ariaDescribedby?: string | undefined;
|
|
11
14
|
}
|
|
12
15
|
declare const PasswordInput: import("svelte").Component<Props, {}, "value">;
|
|
13
16
|
type PasswordInput = ReturnType<typeof PasswordInput>;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import CloneButton from './buttons/CloneButton.svelte';
|
|
14
14
|
import { Button } from './ui/button/index.js';
|
|
15
15
|
import { RefreshCw } from '@lucide/svelte';
|
|
16
|
+
import DataState from './content/DataState.svelte';
|
|
16
17
|
|
|
17
18
|
const i18n = useTranslation();
|
|
18
19
|
|
|
@@ -73,12 +74,13 @@
|
|
|
73
74
|
});
|
|
74
75
|
</script>
|
|
75
76
|
|
|
76
|
-
<div data-svadmin-rendering-resource={activeRendering?.resource.name} data-svadmin-rendering-kind={activeRendering ? 'show' : undefined} class="svadmin-u-3e7ce58d64fa {className}">
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
<div data-svadmin-rendering-resource={activeRendering?.resource.name} data-svadmin-rendering-kind={activeRendering ? 'show' : undefined} data-svadmin-crud-page="show" data-svadmin-resource={resourceName} data-svadmin-record-id={id} class="svadmin-u-3e7ce58d64fa {className}">
|
|
78
|
+
<div data-svadmin-crud-page-header>
|
|
79
|
+
<PageHeader
|
|
80
|
+
title="{resource.label} {i18n.t('common.detail')} #{id}"
|
|
81
|
+
onBack={goBack}
|
|
82
|
+
backLabel={i18n.t('common.back')}
|
|
83
|
+
>
|
|
82
84
|
{#snippet actions()}
|
|
83
85
|
{#if detail.canRead && query.isSuccess && resource.canEdit !== false}
|
|
84
86
|
<EditButton resource={resourceName} recordItemId={id} hideText />
|
|
@@ -98,8 +100,10 @@
|
|
|
98
100
|
{@render headerActions()}
|
|
99
101
|
{/if}
|
|
100
102
|
{/snippet}
|
|
101
|
-
|
|
103
|
+
</PageHeader>
|
|
104
|
+
</div>
|
|
102
105
|
|
|
106
|
+
<div data-svadmin-crud-page-content data-svadmin-detail-state={detail.checkingPermission || query.isLoading ? 'loading' : !detail.canRead ? 'forbidden' : query.isError ? 'error' : query.isSuccess ? 'ready' : 'empty'}>
|
|
103
107
|
{#if detail.checkingPermission || query.isLoading}
|
|
104
108
|
<Card.Root class="svadmin-u-2cd02d11d1af svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
|
|
105
109
|
<Card.Content class="svadmin-u-8a539c7fe216">
|
|
@@ -112,15 +116,11 @@
|
|
|
112
116
|
</Card.Content>
|
|
113
117
|
</Card.Root>
|
|
114
118
|
{:else if !detail.canRead}
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
<div role="alert">
|
|
118
|
-
<p>{i18n.t('common.operationFailed')}</p>
|
|
119
|
-
<Button type="button" variant="ghost" size="icon" title={i18n.t('common.retry')}
|
|
120
|
-
aria-label={i18n.t('common.retry')} disabled={query.isFetching} onclick={detail.refresh}>
|
|
121
|
-
<RefreshCw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
122
|
-
</Button>
|
|
119
|
+
<div data-svadmin-access-denied>
|
|
120
|
+
<DataState state="forbidden" title={i18n.t('common.accessDenied')} />
|
|
123
121
|
</div>
|
|
122
|
+
{:else if query.isError}
|
|
123
|
+
<DataState state="error" title={i18n.t('common.operationFailed')} retry={detail.refresh} />
|
|
124
124
|
{:else if query.isSuccess}
|
|
125
125
|
{@const record = activeRendering ? activeRendering.record(query.data.data) : query.data.data}
|
|
126
126
|
{#if layout === 'grid'}
|
|
@@ -165,13 +165,12 @@
|
|
|
165
165
|
</Card.Root>
|
|
166
166
|
{/if}
|
|
167
167
|
{#if children}
|
|
168
|
-
|
|
168
|
+
<div data-svadmin-crud-page-extension>
|
|
169
|
+
{@render children()}
|
|
170
|
+
</div>
|
|
169
171
|
{/if}
|
|
170
172
|
{:else}
|
|
171
|
-
<
|
|
172
|
-
<Card.Content class="svadmin-u-845f53365c8d svadmin-u-ca6bf63030aa">
|
|
173
|
-
<p class="svadmin-u-bfa603190748">{i18n.t('common.noData')}</p>
|
|
174
|
-
</Card.Content>
|
|
175
|
-
</Card.Root>
|
|
173
|
+
<DataState state="empty" description={i18n.t('common.noData')} />
|
|
176
174
|
{/if}
|
|
175
|
+
</div>
|
|
177
176
|
</div>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { Input } from '../ui/input/index.js';
|
|
21
21
|
import DataState from '../content/DataState.svelte';
|
|
22
22
|
import FeedbackNotice from '../content/FeedbackNotice.svelte';
|
|
23
|
-
import { Copy, Send } from '@lucide/svelte';
|
|
23
|
+
import { Copy, Send, X } from '@lucide/svelte';
|
|
24
24
|
|
|
25
25
|
let { provider }: { provider?: MemberDirectoryProvider | undefined } = $props();
|
|
26
26
|
const context = captureAdminContext();
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
const controlId = $props.id();
|
|
30
30
|
let rows = $state<DirectoryMember[]>([]);
|
|
31
31
|
let query = $state('');
|
|
32
|
+
let searchInput = $state<HTMLInputElement | null>(null);
|
|
32
33
|
let email = $state('');
|
|
33
34
|
let loading = $state(false);
|
|
34
35
|
let sending = $state(false);
|
|
@@ -37,6 +38,10 @@
|
|
|
37
38
|
let status = $state('');
|
|
38
39
|
let epoch = 0;
|
|
39
40
|
const filtered = $derived(rows.filter(row => `${row.name} ${row.email}`.toLowerCase().includes(query.toLowerCase())));
|
|
41
|
+
function clearSearch(): void {
|
|
42
|
+
query = '';
|
|
43
|
+
searchInput?.focus();
|
|
44
|
+
}
|
|
40
45
|
async function load() {
|
|
41
46
|
const current = provider;
|
|
42
47
|
const request = ++epoch;
|
|
@@ -117,10 +122,18 @@
|
|
|
117
122
|
{:else}
|
|
118
123
|
<div class="w-full max-w-sm space-y-2">
|
|
119
124
|
<label for={`${controlId}-search`} class="block text-sm font-medium text-foreground">{isZh ? '搜索成员' : 'Search members'}</label>
|
|
120
|
-
<Input id={`${controlId}-search`} type="search" bind:value={query} />
|
|
125
|
+
<Input id={`${controlId}-search`} type="search" bind:value={query} bind:ref={searchInput} />
|
|
121
126
|
</div>
|
|
122
|
-
{#if
|
|
123
|
-
<DataState state="empty" title={isZh ? '
|
|
127
|
+
{#if rows.length === 0}
|
|
128
|
+
<DataState state="empty" title={isZh ? '暂无成员' : 'No members yet'} />
|
|
129
|
+
{:else if filtered.length === 0}
|
|
130
|
+
<DataState state="filtered-empty" title={isZh ? '没有匹配的成员' : 'No matching members'} description={isZh ? '请清除或调整搜索条件。' : 'Clear or adjust the search.'}>
|
|
131
|
+
{#snippet action()}
|
|
132
|
+
<Button type="button" variant="outline" size="sm" onclick={clearSearch}>
|
|
133
|
+
<X size={16} aria-hidden="true" />{isZh ? '清除搜索' : 'Clear search'}
|
|
134
|
+
</Button>
|
|
135
|
+
{/snippet}
|
|
136
|
+
</DataState>
|
|
124
137
|
{:else}
|
|
125
138
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex (横向滚动区域需支持键盘聚焦与滚动) -->
|
|
126
139
|
<div class="member-directory-scroll w-full min-w-0 overflow-x-auto" role="region" aria-label={isZh ? '成员目录' : 'Member directory'} tabindex="0">
|
|
@@ -23,5 +23,12 @@
|
|
|
23
23
|
<ContentPageHeader title={i18n.t('account.securityLog')} description={i18n.t('account.securityLogDescription')} {...definedOptions({ "actions": undefined })} />
|
|
24
24
|
<div class="svadmin-u-60fbb7713999 svadmin-u-77c08e015d14"><Button variant="outline" size="sm" onclick={() => downloadRows('sample-security-events.json', filtered)}><Download class="svadmin-u-783b0d9d1e2c" />{i18n.t('common.export')}</Button></div>
|
|
25
25
|
<FilterToolbar bind:query placeholder={i18n.t('common.search')} />
|
|
26
|
-
<SecurityEventTable
|
|
26
|
+
<SecurityEventTable
|
|
27
|
+
events={filtered}
|
|
28
|
+
{...definedOptions({
|
|
29
|
+
state: query.trim() && filtered.length === 0 ? 'filtered-empty' : undefined,
|
|
30
|
+
stateTitle: query.trim() && filtered.length === 0 ? (i18n.locale === 'zh-CN' ? '没有匹配的安全事件' : 'No matching security events') : undefined,
|
|
31
|
+
stateDescription: query.trim() && filtered.length === 0 ? (i18n.locale === 'zh-CN' ? '请清除或调整搜索条件。' : 'Clear or adjust the search.') : undefined,
|
|
32
|
+
})}
|
|
33
|
+
/>
|
|
27
34
|
</ContentPageShell>
|
|
@@ -14,14 +14,24 @@
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<script lang="ts">
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
17
19
|
import { cn } from '../../utils.js';
|
|
18
20
|
import StatusBadge from './StatusBadge.svelte';
|
|
19
21
|
import DataState from './DataState.svelte';
|
|
22
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
20
23
|
|
|
21
24
|
interface Props {
|
|
22
25
|
items?: TimelineItem[];
|
|
23
26
|
emptyTitle?: string;
|
|
24
27
|
emptyDescription?: string;
|
|
28
|
+
state?: DataStateKind;
|
|
29
|
+
stateTitle?: string;
|
|
30
|
+
stateDescription?: string;
|
|
31
|
+
retry?: () => void;
|
|
32
|
+
retryLabel?: string;
|
|
33
|
+
loadingLabel?: string;
|
|
34
|
+
action?: Snippet;
|
|
25
35
|
class?: string;
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -29,6 +39,13 @@
|
|
|
29
39
|
items = [],
|
|
30
40
|
emptyTitle = 'No timeline events',
|
|
31
41
|
emptyDescription = 'Activity events and status transitions will appear here.',
|
|
42
|
+
state,
|
|
43
|
+
stateTitle,
|
|
44
|
+
stateDescription,
|
|
45
|
+
retry,
|
|
46
|
+
retryLabel,
|
|
47
|
+
loadingLabel,
|
|
48
|
+
action,
|
|
32
49
|
class: className = '',
|
|
33
50
|
}: Props = $props();
|
|
34
51
|
|
|
@@ -39,13 +56,24 @@
|
|
|
39
56
|
info: 'svadmin-u-75b1bec3ea0e svadmin-u-05f954a846d6 svadmin-u-20aaf08a7ed1',
|
|
40
57
|
neutral: 'svadmin-u-43ed7af3fc8a svadmin-u-18049387f0af svadmin-u-bfa603190748',
|
|
41
58
|
};
|
|
59
|
+
const resolvedState = $derived(state ?? (items.length === 0 ? 'empty' : undefined));
|
|
42
60
|
</script>
|
|
43
61
|
|
|
44
|
-
<div class={cn('svadmin-u-3e7ce58d64fa', className)}>
|
|
45
|
-
{#if
|
|
46
|
-
<DataState
|
|
62
|
+
<div class={cn('svadmin-u-3e7ce58d64fa', className)} data-svadmin-audit-timeline data-svadmin-audit-state={resolvedState ?? 'ready'}>
|
|
63
|
+
{#if resolvedState}
|
|
64
|
+
<DataState
|
|
65
|
+
state={resolvedState}
|
|
66
|
+
{...definedOptions({
|
|
67
|
+
title: stateTitle ?? (resolvedState === 'empty' ? emptyTitle : undefined),
|
|
68
|
+
description: stateDescription ?? (resolvedState === 'empty' ? emptyDescription : undefined),
|
|
69
|
+
retry,
|
|
70
|
+
retryLabel,
|
|
71
|
+
loadingLabel,
|
|
72
|
+
action,
|
|
73
|
+
})}
|
|
74
|
+
/>
|
|
47
75
|
{:else}
|
|
48
|
-
<div class="svadmin-u-d89972fe17d6 svadmin-u-9079b62ef1cb svadmin-u-b3542e058833 svadmin-u-884dada40b17 svadmin-u-699f6fc4b3e0 svadmin-u-37f34e63818d svadmin-u-f76c7493f919 svadmin-u-8f848a57f066 svadmin-u-3242dfaf010c">
|
|
76
|
+
<div class="svadmin-u-d89972fe17d6 svadmin-u-9079b62ef1cb svadmin-u-b3542e058833 svadmin-u-884dada40b17 svadmin-u-699f6fc4b3e0 svadmin-u-37f34e63818d svadmin-u-f76c7493f919 svadmin-u-8f848a57f066 svadmin-u-3242dfaf010c" data-svadmin-audit-events>
|
|
49
77
|
{#each items as item (item.id)}
|
|
50
78
|
{@const statusTone = item.status ?? 'info'}
|
|
51
79
|
<div class="svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-44ee8ba0a421 svadmin-u-fc7473ca09eb group">
|