@svadmin/ui 0.45.0 → 0.47.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.
Files changed (59) hide show
  1. package/dist/components/AboutSettings.svelte +21 -96
  2. package/dist/components/ApiSettings.svelte +54 -234
  3. package/dist/components/AppearanceSettings.svelte +36 -165
  4. package/dist/components/AuthPageShell.svelte +32 -0
  5. package/dist/components/AuthPageShell.svelte.d.ts +11 -0
  6. package/dist/components/AutoForm.svelte +21 -3
  7. package/dist/components/DetailDrawer.svelte +74 -0
  8. package/dist/components/DetailDrawer.svelte.d.ts +16 -0
  9. package/dist/components/FieldRenderer.svelte +70 -2
  10. package/dist/components/FieldRenderer.svelte.d.ts +2 -0
  11. package/dist/components/ForgotPasswordPage.svelte +4 -68
  12. package/dist/components/IntegrationsSettings.svelte +22 -61
  13. package/dist/components/LoginPage.svelte +33 -124
  14. package/dist/components/NotificationsSettings.svelte +22 -103
  15. package/dist/components/PermissionMatrix.svelte +18 -9
  16. package/dist/components/RegisterPage.svelte +7 -62
  17. package/dist/components/RowActions.svelte +154 -0
  18. package/dist/components/RowActions.svelte.d.ts +26 -0
  19. package/dist/components/SecuritySettings.svelte +18 -58
  20. package/dist/components/SettingsPage.svelte +12 -2
  21. package/dist/components/SvadminLogo.svelte +1 -1
  22. package/dist/components/UpdatePasswordPage.svelte +3 -55
  23. package/dist/components/account/CompanyProfilePage.svelte +19 -7
  24. package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
  25. package/dist/components/account/SettingsPlainPage.svelte +25 -3
  26. package/dist/components/account/SettingsSidebarPage.svelte +31 -2
  27. package/dist/components/account/UserProfilePage.svelte +5 -4
  28. package/dist/components/content/ApiKeyList.svelte +40 -6
  29. package/dist/components/content/ApiKeyList.svelte.d.ts +10 -0
  30. package/dist/components/content/DataState.svelte +25 -9
  31. package/dist/components/content/DataState.svelte.d.ts +2 -0
  32. package/dist/components/content/FileList.svelte +12 -4
  33. package/dist/components/content/FileList.svelte.d.ts +9 -0
  34. package/dist/components/content/FilterToolbar.svelte +10 -2
  35. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
  36. package/dist/components/content/MemberList.svelte +13 -5
  37. package/dist/components/content/MemberList.svelte.d.ts +9 -0
  38. package/dist/components/content/MetricBlock.svelte +19 -2
  39. package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
  40. package/dist/components/content/NetworkTable.svelte +9 -3
  41. package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
  42. package/dist/components/content/NetworkUserCard.svelte +47 -7
  43. package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
  44. package/dist/components/content/SectionHeader.svelte +3 -2
  45. package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
  46. package/dist/components/content/SecurityEventTable.svelte +14 -5
  47. package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
  48. package/dist/components/content/SettingsFieldRow.svelte +28 -0
  49. package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
  50. package/dist/components/content/SettingsGroup.svelte +33 -0
  51. package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
  52. package/dist/components/content/WorkspaceLayout.svelte +31 -0
  53. package/dist/components/content/WorkspaceLayout.svelte.d.ts +12 -0
  54. package/dist/components/content/index.d.ts +4 -1
  55. package/dist/components/content/index.js +3 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte +25 -6
  57. package/dist/index.d.ts +9 -1
  58. package/dist/index.js +6 -0
  59. package/package.json +4 -1
@@ -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
 
@@ -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>
@@ -1,117 +1,36 @@
1
1
  <script lang="ts">
2
+ import { useNotification } from '@svadmin/core';
2
3
  import { useTranslation } from '@svadmin/core/i18n';
3
- import { Bell, Mail, Smartphone } from '@lucide/svelte';
4
- import * as Card from './ui/card/index.js';
5
4
  import { Button } from './ui/button/index.js';
6
- import { Label } from './ui/label/index.js';
7
5
  import { Switch } from './ui/switch/index.js';
6
+ import SettingsGroup from './content/SettingsGroup.svelte';
7
+ import SettingsFieldRow from './content/SettingsFieldRow.svelte';
8
8
 
9
9
  const i18n = useTranslation();
10
-
10
+ const notification = useNotification();
11
11
  let emailAlerts = $state({ security: true, activity: false, reports: true });
12
12
  let pushAlerts = $state({ security: true, activity: true, reports: false });
13
13
  let smsAlerts = $state({ security: true });
14
- let isSaved = $state(false);
15
-
16
- function saveSettings() {
17
- isSaved = true;
18
- setTimeout(() => {
19
- isSaved = false;
20
- }, 3000);
21
- }
14
+ function saveSettings() { notification.success(i18n.t('common.autoSaved'), 3000); }
22
15
  </script>
23
16
 
24
17
  <div class="space-y-6">
25
- <div class="flex flex-col justify-between gap-3 sm:flex-row sm:items-start">
26
- <div>
27
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.notifications')}</h2>
28
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.notificationsDescription')}</p>
18
+ <div class="flex flex-col justify-between gap-3 sm:flex-row sm:items-start"><div><h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.notifications')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.notificationsDescription')}</p></div><Button onclick={saveSettings}>{i18n.t('common.save')}</Button></div>
19
+ <SettingsGroup title={i18n.t('notifications.emailTitle')} description={i18n.t('notifications.emailDescription')}>
20
+ <div class="divide-y divide-border">
21
+ <SettingsFieldRow label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityAlertsDesc')}>{#snippet control()}<Switch id="email-security" bind:checked={emailAlerts.security} />{/snippet}</SettingsFieldRow>
22
+ <SettingsFieldRow label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityLogsDesc')}>{#snippet control()}<Switch id="email-activity" bind:checked={emailAlerts.activity} />{/snippet}</SettingsFieldRow>
23
+ <SettingsFieldRow label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemAlertsDesc')}>{#snippet control()}<Switch id="email-reports" bind:checked={emailAlerts.reports} />{/snippet}</SettingsFieldRow>
29
24
  </div>
30
- <Button onclick={saveSettings}>{isSaved ? i18n.t('common.autoSaved') : i18n.t('common.save')}</Button>
31
- </div>
32
-
33
- <Card.Card>
34
- <Card.CardHeader>
35
- <Card.CardTitle class="flex items-center gap-2 text-base">
36
- <Mail class="h-4 w-4 text-muted-foreground" />
37
- {i18n.t('notifications.emailTitle')}
38
- </Card.CardTitle>
39
- <Card.CardDescription>{i18n.t('notifications.emailDescription')}</Card.CardDescription>
40
- </Card.CardHeader>
41
- <Card.CardContent class="space-y-4">
42
- <div class="flex items-center justify-between gap-4 border-b py-2">
43
- <div class="space-y-1">
44
- <Label for="email-security">{i18n.t('notifications.securityAlerts')}</Label>
45
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.securityAlertsDesc')}</p>
46
- </div>
47
- <Switch id="email-security" bind:checked={emailAlerts.security} />
48
- </div>
49
- <div class="flex items-center justify-between gap-4 border-b py-2">
50
- <div class="space-y-1">
51
- <Label for="email-activity">{i18n.t('notifications.activityLogs')}</Label>
52
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.activityLogsDesc')}</p>
53
- </div>
54
- <Switch id="email-activity" bind:checked={emailAlerts.activity} />
55
- </div>
56
- <div class="flex items-center justify-between gap-4 py-2">
57
- <div class="space-y-1">
58
- <Label for="email-reports">{i18n.t('notifications.systemAlerts')}</Label>
59
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.systemAlertsDesc')}</p>
60
- </div>
61
- <Switch id="email-reports" bind:checked={emailAlerts.reports} />
62
- </div>
63
- </Card.CardContent>
64
- </Card.Card>
65
-
66
- <Card.Card>
67
- <Card.CardHeader>
68
- <Card.CardTitle class="flex items-center gap-2 text-base">
69
- <Bell class="h-4 w-4 text-muted-foreground" />
70
- {i18n.t('notifications.pushTitle')}
71
- </Card.CardTitle>
72
- <Card.CardDescription>{i18n.t('notifications.pushDescription')}</Card.CardDescription>
73
- </Card.CardHeader>
74
- <Card.CardContent class="space-y-4">
75
- <div class="flex items-center justify-between gap-4 border-b py-2">
76
- <div class="space-y-1">
77
- <Label for="push-security">{i18n.t('notifications.securityAlerts')}</Label>
78
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.securityPushDesc')}</p>
79
- </div>
80
- <Switch id="push-security" bind:checked={pushAlerts.security} />
81
- </div>
82
- <div class="flex items-center justify-between gap-4 border-b py-2">
83
- <div class="space-y-1">
84
- <Label for="push-activity">{i18n.t('notifications.activityLogs')}</Label>
85
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.activityPushDesc')}</p>
86
- </div>
87
- <Switch id="push-activity" bind:checked={pushAlerts.activity} />
88
- </div>
89
- <div class="flex items-center justify-between gap-4 py-2">
90
- <div class="space-y-1">
91
- <Label for="push-reports">{i18n.t('notifications.systemAlerts')}</Label>
92
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.systemPushDesc')}</p>
93
- </div>
94
- <Switch id="push-reports" bind:checked={pushAlerts.reports} />
95
- </div>
96
- </Card.CardContent>
97
- </Card.Card>
98
-
99
- <Card.Card>
100
- <Card.CardHeader>
101
- <Card.CardTitle class="flex items-center gap-2 text-base">
102
- <Smartphone class="h-4 w-4 text-muted-foreground" />
103
- {i18n.t('notifications.smsTitle')}
104
- </Card.CardTitle>
105
- <Card.CardDescription>{i18n.t('notifications.smsDescription')}</Card.CardDescription>
106
- </Card.CardHeader>
107
- <Card.CardContent>
108
- <div class="flex items-center justify-between gap-4 py-2">
109
- <div class="space-y-1">
110
- <Label for="sms-security">{i18n.t('notifications.criticalSecurity')}</Label>
111
- <p class="text-xs text-muted-foreground">{i18n.t('notifications.criticalSecurityDesc')}</p>
112
- </div>
113
- <Switch id="sms-security" bind:checked={smsAlerts.security} />
114
- </div>
115
- </Card.CardContent>
116
- </Card.Card>
25
+ </SettingsGroup>
26
+ <SettingsGroup title={i18n.t('notifications.pushTitle')} description={i18n.t('notifications.pushDescription')}>
27
+ <div class="divide-y divide-border">
28
+ <SettingsFieldRow label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityPushDesc')}>{#snippet control()}<Switch id="push-security" bind:checked={pushAlerts.security} />{/snippet}</SettingsFieldRow>
29
+ <SettingsFieldRow label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityPushDesc')}>{#snippet control()}<Switch id="push-activity" bind:checked={pushAlerts.activity} />{/snippet}</SettingsFieldRow>
30
+ <SettingsFieldRow label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemPushDesc')}>{#snippet control()}<Switch id="push-reports" bind:checked={pushAlerts.reports} />{/snippet}</SettingsFieldRow>
31
+ </div>
32
+ </SettingsGroup>
33
+ <SettingsGroup title={i18n.t('notifications.smsTitle')} description={i18n.t('notifications.smsDescription')}>
34
+ <SettingsFieldRow label={i18n.t('notifications.criticalSecurity')} description={i18n.t('notifications.criticalSecurityDesc')}>{#snippet control()}<Switch id="sms-security" bind:checked={smsAlerts.security} />{/snippet}</SettingsFieldRow>
35
+ </SettingsGroup>
117
36
  </div>
@@ -56,7 +56,7 @@
56
56
 
57
57
  <div class="h-full flex flex-col pt-2">
58
58
  <!-- Header Title -->
59
- <div class="mb-6 flex items-center justify-between">
59
+ <div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
60
60
  <div>
61
61
  <h1 class="text-2xl font-bold text-foreground flex items-center gap-2">
62
62
  <Shield class="w-6 h-6 text-primary" />
@@ -78,10 +78,19 @@
78
78
  {/if}
79
79
  </div>
80
80
 
81
- <div class="flex flex-col md:flex-row gap-6 flex-1 min-h-[500px]">
81
+ <div class="flex flex-col lg:flex-row gap-6 flex-1 min-h-[500px]">
82
+ <div class="w-full rounded-lg border border-border bg-card p-4 shadow-sm lg:hidden">
83
+ <label for="permission-role-select" class="mb-2 block text-sm font-medium text-foreground">{i18n.t('permissions.roles') || 'Roles'}</label>
84
+ <select id="permission-role-select" bind:value={selectedRole} class="h-9 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" disabled={roles.length === 0}>
85
+ {#each roles as role (role.code)}<option value={role.code}>{role.name}</option>{/each}
86
+ </select>
87
+ {#if roles.length === 0}<p class="mt-2 text-sm text-muted-foreground">{i18n.t('permissions.noRoles') || 'No roles available'}</p>{/if}
88
+ {#if sidebarExtra}<div class="mt-4 border-t border-border pt-4">{@render sidebarExtra()}</div>{/if}
89
+ </div>
90
+
82
91
  <!-- 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>
92
+ <div class="hidden w-full flex-shrink-0 flex-col rounded-lg border border-border bg-card p-4 shadow-sm lg:flex lg:w-64">
93
+ <h2 class="mb-4 px-2 text-sm font-semibold text-muted-foreground">{i18n.t('permissions.roles') || 'Roles'}</h2>
85
94
  <div class="space-y-1 flex-1 overflow-y-auto">
86
95
  {#each roles as role, _i (_i)}
87
96
  <button
@@ -89,7 +98,7 @@
89
98
  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
99
  >
91
100
  {role.name}
92
- <div class="text-[10px] mt-0.5 opacity-60 font-mono tracking-tighter">{role.code}</div>
101
+ <div class="mt-0.5 font-mono text-[10px] opacity-60">{role.code}</div>
93
102
  </button>
94
103
  {/each}
95
104
  {#if roles.length === 0}
@@ -106,10 +115,10 @@
106
115
  </div>
107
116
 
108
117
  <!-- Right: Matrix Grid -->
109
- <div class="flex-1 bg-card rounded-xl shadow-sm border border-border overflow-hidden flex flex-col">
118
+ <div class="flex-1 bg-card rounded-lg shadow-sm border border-border overflow-hidden flex flex-col">
110
119
  <!-- Matrix Header -->
111
- <div class="p-5 border-b border-border bg-muted/30 flex items-center justify-between">
112
- <div class="flex items-center gap-4">
120
+ <div class="p-5 border-b border-border bg-muted/30 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
121
+ <div class="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
113
122
  <div>
114
123
  <h2 class="text-lg font-bold text-foreground">{i18n.t('permissions.currentRole') || 'Current role'}:
115
124
  <span class="text-primary">{roles.find(r => r.code === selectedRole)?.name || (i18n.t('permissions.noneSelected') || 'None selected')}</span>
@@ -156,7 +165,7 @@
156
165
  <!-- Section Grouping Header -->
157
166
  {#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
158
167
  <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>
168
+ <td colspan="{actions.length + 1}" class="px-4 py-2 text-xs font-bold text-primary">{resource.section}</td>
160
169
  </tr>
161
170
  {/if}
162
171
 
@@ -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>