@svadmin/ui 0.44.0 → 0.46.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.
Files changed (49) hide show
  1. package/dist/components/AuthPageShell.svelte +32 -0
  2. package/dist/components/AuthPageShell.svelte.d.ts +11 -0
  3. package/dist/components/ForgotPasswordPage.svelte +6 -74
  4. package/dist/components/LoginPage.svelte +34 -125
  5. package/dist/components/PermissionMatrix.svelte +5 -5
  6. package/dist/components/ProfilePage.svelte +4 -23
  7. package/dist/components/RegisterPage.svelte +7 -62
  8. package/dist/components/SecuritySettings.svelte +4 -10
  9. package/dist/components/SvadminLogo.svelte +1 -1
  10. package/dist/components/Toast.svelte +7 -5
  11. package/dist/components/UpdatePasswordPage.svelte +3 -55
  12. package/dist/components/account/CompanyProfilePage.svelte +11 -5
  13. package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
  14. package/dist/components/account/SettingsPlainPage.svelte +25 -3
  15. package/dist/components/account/SettingsSidebarPage.svelte +31 -2
  16. package/dist/components/content/ApiKeyList.svelte +13 -4
  17. package/dist/components/content/ApiKeyList.svelte.d.ts +7 -0
  18. package/dist/components/content/DataState.svelte +25 -9
  19. package/dist/components/content/DataState.svelte.d.ts +2 -0
  20. package/dist/components/content/FeedbackNotice.svelte +61 -0
  21. package/dist/components/content/FeedbackNotice.svelte.d.ts +13 -0
  22. package/dist/components/content/FileList.svelte +12 -4
  23. package/dist/components/content/FileList.svelte.d.ts +9 -0
  24. package/dist/components/content/FilterToolbar.svelte +10 -2
  25. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
  26. package/dist/components/content/MemberList.svelte +13 -5
  27. package/dist/components/content/MemberList.svelte.d.ts +9 -0
  28. package/dist/components/content/MetricBlock.svelte +19 -2
  29. package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
  30. package/dist/components/content/NetworkTable.svelte +9 -3
  31. package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
  32. package/dist/components/content/NetworkUserCard.svelte +47 -7
  33. package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
  34. package/dist/components/content/SectionHeader.svelte +3 -2
  35. package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
  36. package/dist/components/content/SecurityEventTable.svelte +14 -5
  37. package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
  38. package/dist/components/content/SettingsFieldRow.svelte +28 -0
  39. package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
  40. package/dist/components/content/SettingsGroup.svelte +33 -0
  41. package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
  42. package/dist/components/content/WorkspaceLayout.svelte +28 -0
  43. package/dist/components/content/WorkspaceLayout.svelte.d.ts +11 -0
  44. package/dist/components/content/index.d.ts +6 -1
  45. package/dist/components/content/index.js +4 -0
  46. package/dist/components/network/UserCardsNFTPage.svelte +25 -6
  47. package/dist/index.d.ts +8 -1
  48. package/dist/index.js +5 -0
  49. package/package.json +2 -2
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import SvadminLogo from './SvadminLogo.svelte';
4
+
5
+ interface Props {
6
+ brand: string;
7
+ title: string;
8
+ description?: string;
9
+ children: Snippet;
10
+ footer?: Snippet;
11
+ }
12
+
13
+ let { brand, title, description, children, footer }: Props = $props();
14
+ </script>
15
+
16
+ <div class="grid min-h-screen bg-muted/20 lg:grid-cols-[minmax(18rem,0.72fr)_minmax(0,1.28fr)]" data-svadmin-auth-page>
17
+ <aside class="hidden border-r border-border bg-card p-10 lg:flex lg:flex-col lg:justify-between xl:p-12">
18
+ <div class="flex items-center gap-3"><SvadminLogo size={40} /><span class="text-lg font-semibold text-foreground">{brand}</span></div>
19
+ <div class="max-w-sm space-y-3"><h2 class="text-2xl font-semibold leading-tight text-foreground">{title}</h2>{#if description}<p class="text-sm leading-6 text-muted-foreground">{description}</p>{/if}</div>
20
+ <p class="text-xs text-muted-foreground">{brand}</p>
21
+ </aside>
22
+ <main class="flex items-center justify-center p-5 sm:p-8 lg:p-12">
23
+ <div class="w-full max-w-md space-y-5">
24
+ <div class="flex items-center gap-3 lg:hidden"><SvadminLogo size={36} /><span class="text-base font-semibold text-foreground">{brand}</span></div>
25
+ <section class="rounded-lg border border-border bg-card p-5 shadow-sm sm:p-7">
26
+ <header class="mb-6 space-y-2"><h1 class="text-xl font-semibold leading-tight text-foreground">{title}</h1>{#if description}<p class="text-sm leading-6 text-muted-foreground">{description}</p>{/if}</header>
27
+ {@render children()}
28
+ </section>
29
+ {#if footer}<div class="text-center">{@render footer()}</div>{/if}
30
+ </div>
31
+ </main>
32
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ brand: string;
4
+ title: string;
5
+ description?: string;
6
+ children: Snippet;
7
+ footer?: Snippet;
8
+ }
9
+ declare const AuthPageShell: import("svelte").Component<Props, {}, "">;
10
+ type AuthPageShell = ReturnType<typeof AuthPageShell>;
11
+ export default AuthPageShell;
@@ -6,8 +6,8 @@
6
6
  import { Input } from './ui/input/index.js';
7
7
  import { Label } from './ui/label/index.js';
8
8
  import * as Alert from './ui/alert/index.js';
9
- import SvadminLogo from './SvadminLogo.svelte';
10
- import { User, ArrowLeft, CheckCircle, Loader2, AlertCircle } from '@lucide/svelte';
9
+ import AuthPageShell from './AuthPageShell.svelte';
10
+ import { User, ArrowLeft, Loader2, AlertCircle } from '@lucide/svelte';
11
11
 
12
12
  const i18n = useTranslation();
13
13
 
@@ -16,7 +16,7 @@
16
16
  }>();
17
17
 
18
18
  const adminContext = captureAdminContext();
19
- const forgot = useForgotPassword();
19
+ const forgot = useForgotPassword({ successNotification: false });
20
20
 
21
21
  let identifier = $state('');
22
22
  let error = $state('');
@@ -37,71 +37,9 @@
37
37
  }
38
38
  </script>
39
39
 
40
- <div class="min-h-screen flex flex-col lg:flex-row">
41
- <!-- Left brand panel -->
42
- <div class="hidden lg:flex lg:w-[480px] xl:w-[520px] relative overflow-hidden bg-gradient-to-br from-primary/90 via-primary to-primary/80 shrink-0">
43
- <div class="absolute inset-0">
44
- <div class="absolute top-[12%] left-[8%] w-28 h-28 rounded-3xl border border-white/10 bg-white/5 rotate-12 backdrop-blur-sm"></div>
45
- <div class="absolute top-[55%] left-[55%] w-40 h-40 rounded-full border border-white/10 bg-white/5 -rotate-6 backdrop-blur-sm"></div>
46
- <div class="absolute bottom-[15%] left-[15%] w-24 h-24 rounded-2xl border border-white/10 bg-white/5 -rotate-12 backdrop-blur-sm"></div>
47
- <div class="absolute top-[30%] right-[10%] w-16 h-16 rounded-xl border border-white/10 bg-white/5 rotate-45 backdrop-blur-sm"></div>
48
- <div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
49
- </div>
50
-
51
- <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
52
- <div class="flex items-center gap-3">
53
- <SvadminLogo size={36} onDark />
54
- <span class="text-lg font-semibold text-white tracking-tight">{title}</span>
55
- </div>
56
-
57
- <div class="space-y-4">
58
- <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
59
- {i18n.t('auth.forgotPassword')}
60
- </h2>
61
- <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
62
- {i18n.t('auth.forgotPasswordDescription')}
63
- </p>
64
- </div>
65
-
66
- <div class="flex items-center gap-3">
67
- <div class="h-1 w-8 rounded-full bg-white/30"></div>
68
- <div class="h-1 w-4 rounded-full bg-white/15"></div>
69
- <div class="h-1 w-2 rounded-full bg-white/10"></div>
70
- </div>
71
- </div>
72
- </div>
73
-
74
- <!-- Right form panel -->
75
- <div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
76
- <div class="w-full max-w-[400px]">
77
- <div class="mb-8 flex items-center gap-3 lg:hidden">
78
- <SvadminLogo size={36} />
79
- <span class="text-lg font-semibold tracking-tight">{title}</span>
80
- </div>
81
-
82
- <div class="space-y-1 mb-8">
83
- <h1 class="text-2xl font-semibold tracking-tight">
84
- {#if sent}
85
- {i18n.t('auth.resetLinkSent')}
86
- {:else}
87
- {i18n.t('auth.forgotPassword')}
88
- {/if}
89
- </h1>
90
- <p class="text-sm text-muted-foreground">
91
- {#if sent}
92
- {i18n.t('auth.resetLinkSent')}
93
- {:else}
94
- {i18n.t('auth.forgotPasswordDescription')}
95
- {/if}
96
- </p>
97
- </div>
98
-
40
+ <AuthPageShell brand={title} title={sent ? i18n.t('auth.resetLinkSentTitle') : i18n.t('auth.forgotPassword')} description={sent ? i18n.t('auth.resetLinkSentDescription') : i18n.t('auth.forgotPasswordDescription')}>
99
41
  {#if sent}
100
42
  <div class="space-y-4">
101
- <div class="flex items-center gap-3 rounded-lg border bg-muted/50 p-4">
102
- <CheckCircle class="h-5 w-5 text-primary shrink-0" />
103
- <p class="text-sm text-muted-foreground">{i18n.t('auth.resetLinkSent')}</p>
104
- </div>
105
43
  <Button variant="outline" class="w-full" onclick={() => adminContext.navigate('/login')}>
106
44
  <ArrowLeft class="h-4 w-4 mr-2" />
107
45
  {i18n.t('auth.backToLogin')}
@@ -139,17 +77,11 @@
139
77
  </Button>
140
78
  </form>
141
79
 
142
- <div class="flex items-center justify-center mt-8 pt-6 border-t">
80
+ <div class="mt-6 flex items-center justify-center border-t pt-5">
143
81
  <Button variant="link" class="text-sm h-auto p-0 font-medium inline-flex items-center gap-1" onclick={() => adminContext.navigate('/login')}>
144
82
  <ArrowLeft class="h-3 w-3" />
145
83
  {i18n.t('auth.backToLogin')}
146
84
  </Button>
147
85
  </div>
148
86
  {/if}
149
-
150
- <p class="text-xs text-muted-foreground/60 mt-8 text-center">
151
- Powered by {title}
152
- </p>
153
- </div>
154
- </div>
155
- </div>
87
+ </AuthPageShell>
@@ -5,8 +5,8 @@
5
5
  import { Input } from './ui/input/index.js';
6
6
  import { Label } from './ui/label/index.js';
7
7
  import * as Alert from './ui/alert/index.js';
8
+ import AuthPageShell from './AuthPageShell.svelte';
8
9
  import PasswordInput from './PasswordInput.svelte';
9
- import SvadminLogo from './SvadminLogo.svelte';
10
10
  import { Separator } from './ui/separator/index.js';
11
11
  import { User, Loader2, AlertCircle } from '@lucide/svelte';
12
12
 
@@ -35,7 +35,7 @@
35
35
  }>();
36
36
 
37
37
  const adminContext = captureAdminContext();
38
- const login = useLogin({ errorMessage: false });
38
+ const login = useLogin({ successNotification: false, errorMessage: false });
39
39
  const authProvider = $derived(adminContext.authProvider);
40
40
 
41
41
  function getInitialIdentifier() {
@@ -66,113 +66,67 @@
66
66
  }
67
67
  </script>
68
68
 
69
- <div class="min-h-screen flex flex-col lg:flex-row">
70
- <div class="hidden lg:flex lg:w-[500px] xl:w-[560px] relative overflow-hidden bg-gradient-to-br from-primary/95 via-primary to-primary/85 shrink-0">
71
- <div class="absolute inset-0">
72
- <div class="absolute top-[12%] left-[8%] h-32 w-32 rounded-3xl border border-white/10 bg-white/5 rotate-12 backdrop-blur-sm"></div>
73
- <div class="absolute top-[54%] left-[54%] h-44 w-44 rounded-full border border-white/10 bg-white/5 -rotate-6 backdrop-blur-sm"></div>
74
- <div class="absolute bottom-[14%] left-[13%] h-28 w-28 rounded-2xl border border-white/10 bg-white/5 -rotate-12 backdrop-blur-sm"></div>
75
- <div class="absolute top-[30%] right-[10%] h-20 w-20 rounded-xl border border-white/10 bg-white/5 rotate-45 backdrop-blur-sm"></div>
76
- <div class="absolute inset-0 opacity-[0.04]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
77
- </div>
78
-
79
- <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
80
- <div class="flex items-center gap-3">
81
- <SvadminLogo size={44} onDark />
82
- <span class="text-xl font-semibold text-white tracking-tight">{title}</span>
83
- </div>
84
-
85
- <div class="max-w-[360px] space-y-4">
86
- <h2 class="text-4xl font-bold text-white leading-tight">
87
- {i18n.t('auth.welcomeBack')}
88
- </h2>
89
- <p class="text-lg text-white/75 leading-relaxed">
90
- {i18n.t('auth.welcomeMessage')}
91
- </p>
92
- </div>
93
-
94
- <div class="flex items-center gap-3">
95
- <div class="h-1.5 w-10 rounded-full bg-white/40"></div>
96
- <div class="h-1.5 w-5 rounded-full bg-white/20"></div>
97
- <div class="h-1.5 w-2 rounded-full bg-white/15"></div>
98
- </div>
99
- </div>
100
- </div>
101
-
102
- <div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-10 lg:p-14">
103
- <div class="w-full max-w-[460px] space-y-8">
104
- <div class="mb-8 flex items-center gap-3 lg:hidden">
105
- <SvadminLogo size={44} />
106
- <span class="text-xl font-semibold tracking-tight text-foreground">{title}</span>
107
- </div>
108
-
109
- <div class="space-y-2.5">
110
- <h1 class="text-3xl font-semibold tracking-tight text-foreground">
111
- {i18n.t('auth.welcomeBack')}
112
- </h1>
113
- <p class="text-base text-muted-foreground leading-relaxed">
114
- {i18n.t('auth.welcomeMessage')}
115
- </p>
116
- </div>
69
+ {#snippet footer()}
70
+ {#if authProvider?.register}
71
+ <div class="flex items-center justify-center gap-1.5"><span class="text-sm text-muted-foreground">{i18n.t('auth.noAccount')}</span><Button variant="link" class="h-auto p-0 text-sm font-semibold" onclick={() => adminContext.navigate('/register')}>{i18n.t('auth.register')}</Button></div>
72
+ {/if}
73
+ {/snippet}
117
74
 
75
+ <AuthPageShell brand={title} title={i18n.t('auth.welcomeBack')} description={i18n.t('auth.welcomeMessage')} {footer}>
118
76
  {#if loginHint || defaultIdentifier || defaultPassword}
119
- <div class="rounded-2xl border border-primary/15 bg-primary/[0.06] p-4 shadow-sm">
77
+ <aside class="mb-5 rounded-md border border-border bg-muted/35 p-3">
120
78
  {#if loginHint}
121
- <p class="text-sm font-medium text-primary">{loginHint}</p>
79
+ <p class="text-sm font-medium text-foreground">{loginHint}</p>
122
80
  {/if}
123
- <div class="mt-3 grid gap-2 sm:grid-cols-2">
81
+ <dl class="mt-3 grid gap-2 sm:grid-cols-2">
124
82
  {#if defaultIdentifier}
125
- <div class="rounded-xl border border-border/70 bg-background/80 px-3 py-2">
126
- <p class="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
127
- {i18n.t('auth.usernameOrEmail')}
128
- </p>
129
- <p class="mt-1 font-mono text-sm font-semibold text-foreground">{defaultIdentifier}</p>
83
+ <div class="rounded-md border border-border bg-background px-3 py-2">
84
+ <dt class="text-xs font-medium text-muted-foreground">{i18n.t('auth.usernameOrEmail')}</dt>
85
+ <dd class="mt-1 truncate font-mono text-sm text-foreground">{defaultIdentifier}</dd>
130
86
  </div>
131
87
  {/if}
132
88
  {#if defaultPassword}
133
- <div class="rounded-xl border border-border/70 bg-background/80 px-3 py-2">
134
- <p class="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
135
- {i18n.t('auth.password')}
136
- </p>
137
- <p class="mt-1 font-mono text-sm font-semibold text-foreground">{defaultPassword}</p>
89
+ <div class="rounded-md border border-border bg-background px-3 py-2">
90
+ <dt class="text-xs font-medium text-muted-foreground">{i18n.t('auth.password')}</dt>
91
+ <dd class="mt-1 font-mono text-sm text-foreground">{defaultPassword}</dd>
138
92
  </div>
139
93
  {/if}
140
- </div>
141
- </div>
94
+ </dl>
95
+ </aside>
142
96
  {/if}
143
97
 
144
- <form onsubmit={handleSubmit} class="space-y-6 login-form">
98
+ <form onsubmit={handleSubmit} class="space-y-5">
145
99
  {#if error}
146
- <Alert.Root variant="destructive" class="rounded-xl border border-destructive/20 bg-destructive/5">
100
+ <Alert.Root variant="destructive">
147
101
  <AlertCircle class="h-4 w-4" />
148
102
  <Alert.Description class="text-sm font-medium">{error}</Alert.Description>
149
103
  </Alert.Root>
150
104
  {/if}
151
105
 
152
- <div class="space-y-2.5">
153
- <Label for="login-identifier" class="text-sm font-semibold text-foreground/80 pl-0.5">
106
+ <div class="space-y-2">
107
+ <Label for="login-identifier">
154
108
  {i18n.t('auth.usernameOrEmail')}
155
109
  </Label>
156
110
  <div class="relative">
157
- <User class="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground pointer-events-none z-[1]" />
111
+ <User class="pointer-events-none absolute left-3 top-1/2 z-[1] h-4 w-4 -translate-y-1/2 text-muted-foreground" />
158
112
  <Input
159
113
  id="login-identifier"
160
114
  type="text"
161
115
  placeholder={i18n.t('auth.identifierPlaceholder')}
162
116
  bind:value={identifier}
163
- class="pl-12"
117
+ class="pl-9"
164
118
  autocomplete="username"
165
119
  />
166
120
  </div>
167
121
  </div>
168
122
 
169
- <div class="space-y-2.5">
170
- <div class="flex items-center justify-between pl-0.5">
171
- <Label for="login-password" class="text-sm font-semibold text-foreground/80">
123
+ <div class="space-y-2">
124
+ <div class="flex items-center justify-between">
125
+ <Label for="login-password">
172
126
  {i18n.t('auth.password')}
173
127
  </Label>
174
128
  {#if authProvider?.forgotPassword}
175
- <Button variant="link" class="text-xs h-auto p-0 font-medium hover:text-primary transition-colors" onclick={() => adminContext.navigate('/forgot-password')}>
129
+ <Button variant="link" class="h-auto p-0 text-xs font-medium" onclick={() => adminContext.navigate('/forgot-password')}>
176
130
  {i18n.t('auth.forgotPasswordLink')}
177
131
  </Button>
178
132
  {/if}
@@ -185,7 +139,7 @@
185
139
  />
186
140
  </div>
187
141
 
188
- <Button type="submit" class="w-full h-14 rounded-xl text-base font-semibold shadow-md shadow-primary/20 hover:shadow-lg hover:shadow-primary/25 hover:-translate-y-[1px] active:translate-y-0 transition-all duration-200 mt-2" disabled={login.isLoading}>
142
+ <Button type="submit" class="h-10 w-full" disabled={login.isLoading}>
189
143
  {#if login.isLoading}
190
144
  <Loader2 class="h-5 w-5 animate-spin mr-2" />
191
145
  {/if}
@@ -194,9 +148,9 @@
194
148
  </form>
195
149
 
196
150
  {#if socialProviders.length > 0}
197
- <div class="relative my-8">
151
+ <div class="relative my-6">
198
152
  <Separator />
199
- <span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-4 text-xs font-semibold text-muted-foreground tracking-wider uppercase">
153
+ <span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card px-4 text-xs font-medium text-muted-foreground">
200
154
  {i18n.t('auth.orContinueWith')}
201
155
  </span>
202
156
  </div>
@@ -205,7 +159,7 @@
205
159
  {#each socialProviders as provider (provider.name)}
206
160
  <Button
207
161
  variant="outline"
208
- class="w-full h-12 rounded-xl font-semibold border-input/60 hover:bg-muted/50 hover:text-accent-foreground transition-all duration-200"
162
+ class="h-10 w-full"
209
163
  onclick={provider.onClick}
210
164
  >
211
165
  {#if provider.icon}
@@ -216,49 +170,4 @@
216
170
  {/each}
217
171
  </div>
218
172
  {/if}
219
-
220
- {#if authProvider?.register}
221
- <div class="flex items-center justify-center gap-1.5 mt-8 pt-6 border-t border-border/60">
222
- <span class="text-sm text-muted-foreground">{i18n.t('auth.noAccount')}</span>
223
- <Button variant="link" class="text-sm h-auto p-0 font-semibold text-primary hover:underline" onclick={() => adminContext.navigate('/register')}>
224
- {i18n.t('auth.register')}
225
- </Button>
226
- </div>
227
- {/if}
228
-
229
- <p class="text-xs text-muted-foreground/50 mt-8 text-center tracking-wide font-medium">
230
- Powered by {title}
231
- </p>
232
- </div>
233
- </div>
234
- </div>
235
-
236
- <style>
237
- .login-form :global(input[data-slot='input']) {
238
- height: 3.5rem;
239
- border-radius: 0.75rem;
240
- border-color: color-mix(in oklch, var(--border) 88%, var(--foreground));
241
- background-color: var(--background);
242
- font-size: 0.975rem;
243
- transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
244
- }
245
-
246
- .login-form :global(input[data-slot='input']:focus-visible) {
247
- border-color: var(--primary);
248
- box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 18%, transparent);
249
- }
250
-
251
- .login-form :global(.relative > svg:first-of-type) {
252
- left: 1rem;
253
- height: 1.25rem;
254
- width: 1.25rem;
255
- color: color-mix(in oklch, var(--muted-foreground) 82%, var(--foreground));
256
- }
257
-
258
- .login-form :global(.relative > button) {
259
- right: 0.625rem;
260
- height: 2.25rem;
261
- width: 2.25rem;
262
- border-radius: 0.5rem;
263
- }
264
- </style>
173
+ </AuthPageShell>
@@ -80,8 +80,8 @@
80
80
 
81
81
  <div class="flex flex-col md:flex-row gap-6 flex-1 min-h-[500px]">
82
82
  <!-- Left: Role Selector Sidebar -->
83
- <div class="w-full md:w-64 flex-shrink-0 bg-card rounded-xl shadow-sm border border-border p-4 flex flex-col">
84
- <h2 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">{i18n.t('permissions.roles') || 'Roles'}</h2>
83
+ <div class="w-full md:w-64 flex-shrink-0 bg-card rounded-lg shadow-sm border border-border p-4 flex flex-col">
84
+ <h2 class="text-sm font-semibold text-muted-foregroundr mb-4 px-2">{i18n.t('permissions.roles') || 'Roles'}</h2>
85
85
  <div class="space-y-1 flex-1 overflow-y-auto">
86
86
  {#each roles as role, _i (_i)}
87
87
  <button
@@ -89,7 +89,7 @@
89
89
  class="w-full text-left px-3 py-2.5 rounded-lg text-sm transition-all duration-200 {selectedRole === role.code ? 'bg-primary/10 text-primary font-medium shadow-sm ring-1 ring-primary/20' : 'text-foreground hover:bg-muted'}"
90
90
  >
91
91
  {role.name}
92
- <div class="text-[10px] mt-0.5 opacity-60 font-mono tracking-tighter">{role.code}</div>
92
+ <div class="text-[10px] mt-0.5 opacity-60 font-monoer">{role.code}</div>
93
93
  </button>
94
94
  {/each}
95
95
  {#if roles.length === 0}
@@ -106,7 +106,7 @@
106
106
  </div>
107
107
 
108
108
  <!-- Right: Matrix Grid -->
109
- <div class="flex-1 bg-card rounded-xl shadow-sm border border-border overflow-hidden flex flex-col">
109
+ <div class="flex-1 bg-card rounded-lg shadow-sm border border-border overflow-hidden flex flex-col">
110
110
  <!-- Matrix Header -->
111
111
  <div class="p-5 border-b border-border bg-muted/30 flex items-center justify-between">
112
112
  <div class="flex items-center gap-4">
@@ -156,7 +156,7 @@
156
156
  <!-- Section Grouping Header -->
157
157
  {#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
158
158
  <tr class="bg-primary/5">
159
- <td colspan="{actions.length + 1}" class="px-4 py-2 text-xs font-bold text-primary uppercase tracking-wider">{resource.section}</td>
159
+ <td colspan="{actions.length + 1}" class="px-4 py-2 text-xs font-bold text-primaryr">{resource.section}</td>
160
160
  </tr>
161
161
  {/if}
162
162
 
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  captureAdminContext,
4
4
  useGetIdentity,
5
+ useNotification,
5
6
  useUpdatePassword,
6
7
  type AuthProvider,
7
8
  } from '@svadmin/core';
@@ -12,12 +13,13 @@
12
13
  import * as Card from './ui/card/index.js';
13
14
  import * as Alert from './ui/alert/index.js';
14
15
  import PasswordInput from './PasswordInput.svelte';
15
- import { User, Mail, Lock, Loader2, AlertCircle, CheckCircle, Camera } from '@lucide/svelte';
16
+ import { User, Mail, Lock, Loader2, AlertCircle, Camera } from '@lucide/svelte';
16
17
 
17
18
  const i18n = useTranslation();
18
19
  const adminContext = captureAdminContext();
19
20
 
20
21
  const identity = useGetIdentity();
22
+ const notification = useNotification();
21
23
  const updatePw = useUpdatePassword();
22
24
 
23
25
  const authProvider = $derived(adminContext.authProvider);
@@ -27,7 +29,6 @@
27
29
  let editingProfile = $state(false);
28
30
  let editName = $state('');
29
31
  let profileSaving = $state(false);
30
- let profileSuccess = $state(false);
31
32
  let profileError = $state('');
32
33
  let avatarUploading = $state(false);
33
34
  let fileInput = $state<HTMLInputElement>();
@@ -35,21 +36,18 @@
35
36
  let newPassword = $state('');
36
37
  let confirmPassword = $state('');
37
38
  let pwError = $state('');
38
- let pwSuccess = $state(false);
39
39
  let scopeEpoch = 0;
40
40
 
41
41
  function resetProfileScope(): void {
42
42
  editingProfile = false;
43
43
  editName = '';
44
44
  profileSaving = false;
45
- profileSuccess = false;
46
45
  profileError = '';
47
46
  avatarUploading = false;
48
47
  currentPassword = '';
49
48
  newPassword = '';
50
49
  confirmPassword = '';
51
50
  pwError = '';
52
- pwSuccess = false;
53
51
  }
54
52
 
55
53
  function isActiveScope(
@@ -75,7 +73,6 @@
75
73
  function startEditProfile() {
76
74
  editName = identity.data?.name ?? '';
77
75
  editingProfile = true;
78
- profileSuccess = false;
79
76
  profileError = '';
80
77
  }
81
78
 
@@ -90,9 +87,9 @@
90
87
  const result = await provider.updateProfile({ name: editName });
91
88
  if (!isActiveScope(epoch, provider, scopedTenantIdentity)) return;
92
89
  if (result.success) {
93
- profileSuccess = true;
94
90
  editingProfile = false;
95
91
  identity.refetch();
92
+ notification.success(i18n.t('common.updateSuccess'), 3000);
96
93
  } else {
97
94
  profileError = result.error?.message ?? i18n.t('common.operationFailed');
98
95
  }
@@ -123,7 +120,6 @@
123
120
  async function handlePasswordChange(e: SubmitEvent) {
124
121
  e.preventDefault();
125
122
  pwError = '';
126
- pwSuccess = false;
127
123
 
128
124
  if (!newPassword) { pwError = i18n.t('auth.passwordRequired'); return; }
129
125
  if (newPassword !== confirmPassword) { pwError = i18n.t('auth.passwordMismatch'); return; }
@@ -138,7 +134,6 @@
138
134
  });
139
135
  if (!isActiveScope(epoch, provider, scopedTenantIdentity)) return;
140
136
  if (result.success) {
141
- pwSuccess = true;
142
137
  currentPassword = '';
143
138
  newPassword = '';
144
139
  confirmPassword = '';
@@ -174,13 +169,6 @@
174
169
  {i18n.t('common.loading')}
175
170
  </div>
176
171
  {:else if identity.data}
177
- {#if profileSuccess}
178
- <Alert.Root class="border-success/30 bg-success/5 text-success mb-4">
179
- <CheckCircle class="h-4 w-4" />
180
- <Alert.Description>{i18n.t('common.updateSuccess')}</Alert.Description>
181
- </Alert.Root>
182
- {/if}
183
-
184
172
  <div class="flex items-start gap-6">
185
173
  <!-- Avatar -->
186
174
  <div class="shrink-0 relative group">
@@ -292,13 +280,6 @@
292
280
  </Alert.Root>
293
281
  {/if}
294
282
 
295
- {#if pwSuccess}
296
- <Alert.Root class="border-success/30 bg-success/5 text-success">
297
- <CheckCircle class="h-4 w-4" />
298
- <Alert.Description>{i18n.t('profile.passwordChanged')}</Alert.Description>
299
- </Alert.Root>
300
- {/if}
301
-
302
283
  <PasswordInput
303
284
  id="current-password"
304
285
  label={i18n.t('profile.currentPassword')}
@@ -5,8 +5,8 @@
5
5
  import { Input } from './ui/input/index.js';
6
6
  import { Label } from './ui/label/index.js';
7
7
  import * as Alert from './ui/alert/index.js';
8
+ import AuthPageShell from './AuthPageShell.svelte';
8
9
  import PasswordInput from './PasswordInput.svelte';
9
- import SvadminLogo from './SvadminLogo.svelte';
10
10
  import { User, Loader2, AlertCircle } from '@lucide/svelte';
11
11
 
12
12
  const i18n = useTranslation();
@@ -41,53 +41,11 @@
41
41
  }
42
42
  </script>
43
43
 
44
- <div class="min-h-screen flex flex-col lg:flex-row">
45
- <!-- Left brand panel (mirrors LoginPage) -->
46
- <div class="hidden lg:flex lg:w-[480px] xl:w-[520px] relative overflow-hidden bg-gradient-to-br from-primary/90 via-primary to-primary/80 shrink-0">
47
- <div class="absolute inset-0">
48
- <div class="absolute top-[12%] left-[8%] w-28 h-28 rounded-3xl border border-white/10 bg-white/5 rotate-12 backdrop-blur-sm"></div>
49
- <div class="absolute top-[55%] left-[55%] w-40 h-40 rounded-full border border-white/10 bg-white/5 -rotate-6 backdrop-blur-sm"></div>
50
- <div class="absolute bottom-[15%] left-[15%] w-24 h-24 rounded-2xl border border-white/10 bg-white/5 -rotate-12 backdrop-blur-sm"></div>
51
- <div class="absolute top-[30%] right-[10%] w-16 h-16 rounded-xl border border-white/10 bg-white/5 rotate-45 backdrop-blur-sm"></div>
52
- <div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
53
- </div>
54
-
55
- <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
56
- <div class="flex items-center gap-3">
57
- <SvadminLogo size={36} onDark />
58
- <span class="text-lg font-semibold text-white tracking-tight">{title}</span>
59
- </div>
60
-
61
- <div class="space-y-4">
62
- <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
63
- {i18n.t('auth.createAccount')}
64
- </h2>
65
- <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
66
- {i18n.t('auth.createAccountMessage')}
67
- </p>
68
- </div>
69
-
70
- <div class="flex items-center gap-3">
71
- <div class="h-1 w-8 rounded-full bg-white/30"></div>
72
- <div class="h-1 w-4 rounded-full bg-white/15"></div>
73
- <div class="h-1 w-2 rounded-full bg-white/10"></div>
74
- </div>
75
- </div>
76
- </div>
77
-
78
- <!-- Right form panel -->
79
- <div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
80
- <div class="w-full max-w-[400px]">
81
- <div class="mb-8 flex items-center gap-3 lg:hidden">
82
- <SvadminLogo size={36} />
83
- <span class="text-lg font-semibold tracking-tight">{title}</span>
84
- </div>
85
-
86
- <div class="space-y-1 mb-8">
87
- <h1 class="text-2xl font-semibold tracking-tight">{i18n.t('auth.createAccount')}</h1>
88
- <p class="text-sm text-muted-foreground">{i18n.t('auth.createAccountMessage')}</p>
89
- </div>
44
+ {#snippet footer()}
45
+ <div class="flex items-center justify-center gap-1"><span class="text-sm text-muted-foreground">{i18n.t('auth.hasAccount')}</span><Button variant="link" class="h-auto p-0 text-sm font-medium" onclick={() => adminContext.navigate('/login')}>{i18n.t('auth.login')}</Button></div>
46
+ {/snippet}
90
47
 
48
+ <AuthPageShell brand={title} title={i18n.t('auth.createAccount')} description={i18n.t('auth.createAccountMessage')} {footer}>
91
49
  <form onsubmit={handleSubmit} class="space-y-5">
92
50
  {#if error}
93
51
  <Alert.Root variant="destructive">
@@ -126,24 +84,11 @@
126
84
  autocomplete="new-password"
127
85
  />
128
86
 
129
- <Button type="submit" class="w-full h-10" disabled={register.isLoading}>
87
+ <Button type="submit" class="h-10 w-full" disabled={register.isLoading}>
130
88
  {#if register.isLoading}
131
89
  <Loader2 class="h-4 w-4 animate-spin mr-2" />
132
90
  {/if}
133
91
  {i18n.t('auth.registerButton')}
134
92
  </Button>
135
93
  </form>
136
-
137
- <div class="flex items-center justify-center gap-1 mt-8 pt-6 border-t">
138
- <span class="text-sm text-muted-foreground">{i18n.t('auth.hasAccount')}</span>
139
- <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => adminContext.navigate('/login')}>
140
- {i18n.t('auth.login')}
141
- </Button>
142
- </div>
143
-
144
- <p class="text-xs text-muted-foreground/60 mt-8 text-center">
145
- Powered by {title}
146
- </p>
147
- </div>
148
- </div>
149
- </div>
94
+ </AuthPageShell>