@svadmin/ui 0.4.1 → 0.5.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"author": "zuohuadong",
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
|
43
|
-
"url": "
|
|
43
|
+
"url": "https://github.com/zuohuadong/svadmin.git",
|
|
44
|
+
"directory": "packages/ui"
|
|
44
45
|
},
|
|
45
46
|
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
46
47
|
"bugs": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import type { Action } from '@svadmin/core
|
|
3
|
+
import { useCan } from '@svadmin/core';
|
|
4
|
+
import type { Action } from '@svadmin/core';
|
|
5
5
|
|
|
6
6
|
let { resource, action, params, children, fallback } = $props<{
|
|
7
7
|
resource: string;
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
fallback?: Snippet;
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const can = $derived(useCan(resource, action, params));
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
|
-
{#if
|
|
17
|
+
{#if can.isLoading}
|
|
18
|
+
<!-- Loading: nothing rendered -->
|
|
19
|
+
{:else if can.allowed}
|
|
18
20
|
{@render children()}
|
|
19
21
|
{:else if fallback}
|
|
20
22
|
{@render fallback()}
|
|
@@ -8,11 +8,34 @@
|
|
|
8
8
|
import * as Card from './ui/card/index.js';
|
|
9
9
|
import * as Alert from './ui/alert/index.js';
|
|
10
10
|
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
import { Separator } from './ui/separator/index.js';
|
|
11
12
|
import { LogIn, Mail, Loader2, AlertCircle } from 'lucide-svelte';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Social provider config for the login page.
|
|
16
|
+
* Each entry renders a button that calls `onClick()` when clicked.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const socialProviders = [
|
|
21
|
+
* { name: 'Google', icon: '🔵', onClick: () => googleAuth.login({}) },
|
|
22
|
+
* { name: 'GitHub', icon: '🐙', onClick: () => githubAuth.login({}) },
|
|
23
|
+
* ];
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
interface SocialProvider {
|
|
27
|
+
/** Display name (e.g., 'Google') */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Icon character, emoji, or SVG string */
|
|
30
|
+
icon?: string;
|
|
31
|
+
/** Handler to initiate social login */
|
|
32
|
+
onClick: () => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let { title = 'Admin', onSuccess, socialProviders = [] } = $props<{
|
|
14
36
|
title?: string;
|
|
15
37
|
onSuccess?: () => void;
|
|
38
|
+
socialProviders?: SocialProvider[];
|
|
16
39
|
}>();
|
|
17
40
|
|
|
18
41
|
const login = useLogin();
|
|
@@ -97,6 +120,30 @@
|
|
|
97
120
|
</Button>
|
|
98
121
|
</form>
|
|
99
122
|
|
|
123
|
+
{#if socialProviders.length > 0}
|
|
124
|
+
<div class="relative my-5">
|
|
125
|
+
<Separator />
|
|
126
|
+
<span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-xs text-muted-foreground">
|
|
127
|
+
{t('auth.orContinueWith')}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="grid gap-2" class:grid-cols-2={socialProviders.length >= 2}>
|
|
132
|
+
{#each socialProviders as provider (provider.name)}
|
|
133
|
+
<Button
|
|
134
|
+
variant="outline"
|
|
135
|
+
class="w-full"
|
|
136
|
+
onclick={provider.onClick}
|
|
137
|
+
>
|
|
138
|
+
{#if provider.icon}
|
|
139
|
+
<span class="mr-2">{provider.icon}</span>
|
|
140
|
+
{/if}
|
|
141
|
+
{provider.name}
|
|
142
|
+
</Button>
|
|
143
|
+
{/each}
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
100
147
|
{#if authProvider.register}
|
|
101
148
|
<div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
|
|
102
149
|
<span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useGetIdentity, useUpdatePassword } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Label } from './ui/label/index.js';
|
|
6
|
+
import * as Card from './ui/card/index.js';
|
|
7
|
+
import * as Alert from './ui/alert/index.js';
|
|
8
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
9
|
+
import { User, Mail, Lock, Loader2, AlertCircle, CheckCircle } from 'lucide-svelte';
|
|
10
|
+
|
|
11
|
+
const identity = useGetIdentity();
|
|
12
|
+
const updatePw = useUpdatePassword();
|
|
13
|
+
|
|
14
|
+
// Password change form
|
|
15
|
+
let currentPassword = $state('');
|
|
16
|
+
let newPassword = $state('');
|
|
17
|
+
let confirmPassword = $state('');
|
|
18
|
+
let pwError = $state('');
|
|
19
|
+
let pwSuccess = $state(false);
|
|
20
|
+
|
|
21
|
+
async function handlePasswordChange(e: SubmitEvent) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
pwError = '';
|
|
24
|
+
pwSuccess = false;
|
|
25
|
+
|
|
26
|
+
if (!newPassword) { pwError = t('auth.passwordRequired'); return; }
|
|
27
|
+
if (newPassword !== confirmPassword) { pwError = t('auth.passwordMismatch'); return; }
|
|
28
|
+
|
|
29
|
+
const result = await updatePw.mutate({
|
|
30
|
+
password: newPassword,
|
|
31
|
+
currentPassword,
|
|
32
|
+
confirmPassword,
|
|
33
|
+
});
|
|
34
|
+
if (result.success) {
|
|
35
|
+
pwSuccess = true;
|
|
36
|
+
currentPassword = '';
|
|
37
|
+
newPassword = '';
|
|
38
|
+
confirmPassword = '';
|
|
39
|
+
} else {
|
|
40
|
+
pwError = result.error?.message ?? t('common.operationFailed');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Avatar initials
|
|
45
|
+
const initials = $derived(
|
|
46
|
+
identity.data?.name
|
|
47
|
+
? identity.data.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
|
|
48
|
+
: '?'
|
|
49
|
+
);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="max-w-2xl mx-auto space-y-6 p-4 sm:p-6">
|
|
53
|
+
<!-- Profile Info -->
|
|
54
|
+
<Card.Card>
|
|
55
|
+
<Card.CardHeader>
|
|
56
|
+
<Card.CardTitle class="flex items-center gap-2">
|
|
57
|
+
<User class="h-5 w-5" />
|
|
58
|
+
{t('profile.title')}
|
|
59
|
+
</Card.CardTitle>
|
|
60
|
+
</Card.CardHeader>
|
|
61
|
+
<Card.CardContent>
|
|
62
|
+
{#if identity.isLoading}
|
|
63
|
+
<div class="flex items-center gap-2 text-muted-foreground">
|
|
64
|
+
<Loader2 class="h-4 w-4 animate-spin" />
|
|
65
|
+
{t('common.loading')}
|
|
66
|
+
</div>
|
|
67
|
+
{:else if identity.data}
|
|
68
|
+
<div class="flex items-start gap-6">
|
|
69
|
+
<!-- Avatar -->
|
|
70
|
+
<div class="shrink-0">
|
|
71
|
+
{#if identity.data.avatar}
|
|
72
|
+
<img
|
|
73
|
+
src={identity.data.avatar}
|
|
74
|
+
alt={identity.data.name ?? ''}
|
|
75
|
+
class="h-20 w-20 rounded-xl object-cover ring-2 ring-primary/20"
|
|
76
|
+
/>
|
|
77
|
+
{:else}
|
|
78
|
+
<div class="h-20 w-20 rounded-xl bg-primary/10 text-primary flex items-center justify-center text-2xl font-bold">
|
|
79
|
+
{initials}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Info -->
|
|
85
|
+
<div class="space-y-3 flex-1 min-w-0">
|
|
86
|
+
<div>
|
|
87
|
+
<Label class="text-xs text-muted-foreground">{t('profile.name')}</Label>
|
|
88
|
+
<p class="text-lg font-semibold truncate">{identity.data.name ?? '—'}</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{#if identity.data.email}
|
|
92
|
+
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
93
|
+
<Mail class="h-4 w-4 shrink-0" />
|
|
94
|
+
<span class="truncate">{identity.data.email}</span>
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
97
|
+
|
|
98
|
+
{#if identity.data.id}
|
|
99
|
+
<div class="text-xs text-muted-foreground/60 font-mono">
|
|
100
|
+
ID: {identity.data.id}
|
|
101
|
+
</div>
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
{:else}
|
|
106
|
+
<p class="text-sm text-muted-foreground">{t('profile.notAvailable')}</p>
|
|
107
|
+
{/if}
|
|
108
|
+
</Card.CardContent>
|
|
109
|
+
</Card.Card>
|
|
110
|
+
|
|
111
|
+
<!-- Change Password -->
|
|
112
|
+
<Card.Card>
|
|
113
|
+
<Card.CardHeader>
|
|
114
|
+
<Card.CardTitle class="flex items-center gap-2">
|
|
115
|
+
<Lock class="h-5 w-5" />
|
|
116
|
+
{t('profile.changePassword')}
|
|
117
|
+
</Card.CardTitle>
|
|
118
|
+
</Card.CardHeader>
|
|
119
|
+
<Card.CardContent>
|
|
120
|
+
<form onsubmit={handlePasswordChange} class="space-y-4 max-w-sm">
|
|
121
|
+
{#if pwError}
|
|
122
|
+
<Alert.Root variant="destructive">
|
|
123
|
+
<AlertCircle class="h-4 w-4" />
|
|
124
|
+
<Alert.Description>{pwError}</Alert.Description>
|
|
125
|
+
</Alert.Root>
|
|
126
|
+
{/if}
|
|
127
|
+
|
|
128
|
+
{#if pwSuccess}
|
|
129
|
+
<Alert.Root class="border-green-500/30 bg-green-500/5 text-green-600 dark:text-green-400">
|
|
130
|
+
<CheckCircle class="h-4 w-4" />
|
|
131
|
+
<Alert.Description>{t('profile.passwordChanged')}</Alert.Description>
|
|
132
|
+
</Alert.Root>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
<PasswordInput
|
|
136
|
+
id="current-password"
|
|
137
|
+
label={t('profile.currentPassword')}
|
|
138
|
+
bind:value={currentPassword}
|
|
139
|
+
autocomplete="current-password"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<PasswordInput
|
|
143
|
+
id="new-password"
|
|
144
|
+
label={t('auth.password')}
|
|
145
|
+
bind:value={newPassword}
|
|
146
|
+
autocomplete="new-password"
|
|
147
|
+
showStrength
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<PasswordInput
|
|
151
|
+
id="confirm-new-password"
|
|
152
|
+
label={t('auth.confirmPassword')}
|
|
153
|
+
bind:value={confirmPassword}
|
|
154
|
+
autocomplete="new-password"
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
<Button type="submit" disabled={updatePw.isLoading}>
|
|
158
|
+
{#if updatePw.isLoading}
|
|
159
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
160
|
+
{/if}
|
|
161
|
+
{t('profile.updatePassword')}
|
|
162
|
+
</Button>
|
|
163
|
+
</form>
|
|
164
|
+
</Card.CardContent>
|
|
165
|
+
</Card.Card>
|
|
166
|
+
</div>
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { default as DrawerForm } from './components/DrawerForm.svelte';
|
|
|
28
28
|
export { default as DevTools } from './components/DevTools.svelte';
|
|
29
29
|
export { default as Authenticated } from './components/Authenticated.svelte';
|
|
30
30
|
export { default as UpdatePasswordPage } from './components/UpdatePasswordPage.svelte';
|
|
31
|
+
export { default as ProfilePage } from './components/ProfilePage.svelte';
|
|
31
32
|
export { default as ConfigErrorScreen } from './components/ConfigErrorScreen.svelte';
|
|
32
33
|
export { default as InferencerPanel } from './components/InferencerPanel.svelte';
|
|
33
34
|
export { default as LiveIndicator } from './components/LiveIndicator.svelte';
|