@svadmin/ui 0.34.10 → 0.36.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 (71) hide show
  1. package/dist/app.css +746 -22
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/components/AdminApp.svelte +94 -15
  4. package/dist/components/AdminApp.svelte.d.ts +32 -2
  5. package/dist/components/ApiSettings.svelte +191 -0
  6. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  7. package/dist/components/AutoForm.svelte +47 -42
  8. package/dist/components/AutoTable.svelte +29 -24
  9. package/dist/components/ErrorPage.svelte +58 -0
  10. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  11. package/dist/components/FieldRenderer.svelte +2 -2
  12. package/dist/components/ForgotPasswordPage.svelte +111 -68
  13. package/dist/components/Header.svelte +6 -7
  14. package/dist/components/IntegrationsSettings.svelte +70 -0
  15. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  16. package/dist/components/Layout.svelte +7 -7
  17. package/dist/components/LoginPage.svelte +207 -105
  18. package/dist/components/LoginPage.svelte.d.ts +3 -15
  19. package/dist/components/NotificationsSettings.svelte +115 -0
  20. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  21. package/dist/components/PageHeader.svelte +9 -7
  22. package/dist/components/RegisterPage.svelte +103 -65
  23. package/dist/components/ResourceOperationsPage.svelte +151 -0
  24. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  25. package/dist/components/SecuritySettings.svelte +179 -0
  26. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  27. package/dist/components/SettingsPage.svelte +120 -87
  28. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  29. package/dist/components/ShowPage.svelte +9 -9
  30. package/dist/components/Sidebar.svelte +278 -189
  31. package/dist/components/SidebarItem.svelte +77 -18
  32. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  33. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  34. package/dist/components/UpdatePasswordPage.svelte +86 -48
  35. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  36. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  37. package/dist/components/account/GetStartedPage.svelte +123 -0
  38. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  39. package/dist/components/account/ImportMembersPage.svelte +143 -0
  40. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  41. package/dist/components/account/MembersStarterPage.svelte +104 -0
  42. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  43. package/dist/components/account/SecurityLogPage.svelte +135 -0
  44. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  45. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  46. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  47. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  48. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  49. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  50. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  51. package/dist/components/account/TeamMembersPage.svelte +98 -0
  52. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  53. package/dist/components/network/TeamCrewTablePage.svelte +155 -0
  54. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  55. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  57. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  58. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  59. package/dist/components/profile/ProfileCard.svelte +216 -0
  60. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  61. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  62. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  63. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  64. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  65. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  66. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  67. package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
  68. package/dist/index.d.ts +23 -0
  69. package/dist/index.js +27 -0
  70. package/dist/router-state.svelte.js +17 -0
  71. package/package.json +2 -2
@@ -5,7 +5,9 @@
5
5
  import SidebarItem from './SidebarItem.svelte';
6
6
  import {
7
7
  LayoutDashboard, FileText, Users, Settings, Home,
8
- ChevronDown, Folder, ExternalLink, type Icon as LucideIcon,
8
+ ChevronDown, Folder, ExternalLink, Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle,
9
+ Barcode, FolderTree, Building2, Warehouse, BriefcaseBusiness, Handshake, UserRoundCheck,
10
+ CalendarCheck, KeyRound, MapPinned,
9
11
  } from '@lucide/svelte';
10
12
 
11
13
  import { formatLink } from '@svadmin/core/router';
@@ -24,9 +26,23 @@
24
26
  settings: Settings,
25
27
  home: Home,
26
28
  folder: Folder,
29
+ repeat: Repeat,
30
+ 'clipboard-check': ClipboardCheck,
31
+ 'sliders-horizontal': SlidersHorizontal,
32
+ 'alert-triangle': AlertTriangle,
33
+ 'barcode': Barcode,
34
+ 'folder-tree': FolderTree,
35
+ 'building-2': Building2,
36
+ 'warehouse': Warehouse,
37
+ 'briefcase-business': BriefcaseBusiness,
38
+ handshake: Handshake,
39
+ 'user-round-check': UserRoundCheck,
40
+ 'calendar-check': CalendarCheck,
41
+ 'key-round': KeyRound,
42
+ 'map-pinned': MapPinned,
27
43
  };
28
44
 
29
- function getIcon(name?: string | any): typeof LayoutDashboard {
45
+ function getIcon(name?: string | typeof LayoutDashboard): typeof LayoutDashboard {
30
46
  if (!name) return depth === 0 ? Settings : Folder;
31
47
  if (typeof name !== 'string') return name; // if it's already a component
32
48
  return iconMap[name] ?? Settings;
@@ -48,16 +64,41 @@
48
64
  return menuItem.children?.some((c: MenuItem) => hasActiveChild(c)) ?? false;
49
65
  }
50
66
 
67
+ function getItemBadge(menuItem: MenuItem): string | undefined {
68
+ const meta = menuItem.meta as (MenuItem['meta'] & { badge?: unknown }) | undefined;
69
+ return typeof meta?.badge === 'string' ? meta.badge : undefined;
70
+ }
71
+
72
+ function aggregateBadges(menuItems: MenuItem[] | undefined): string | undefined {
73
+ const total = menuItems?.reduce((sum, menuItem) => {
74
+ const ownBadge = Number(getItemBadge(menuItem));
75
+ const childBadge = Number(aggregateBadges(menuItem.children));
76
+ return sum + (Number.isFinite(ownBadge) ? ownBadge : 0) + (Number.isFinite(childBadge) ? childBadge : 0);
77
+ }, 0) ?? 0;
78
+
79
+ return total > 0 ? String(total) : undefined;
80
+ }
81
+
51
82
  const hasChildren = $derived(item.children && item.children.length > 0);
52
83
  const active = $derived(isActive(item.href));
53
84
  const childActive = $derived(hasActiveChild(item));
54
85
  const isExternal = $derived(item.target === '_blank' || item.href?.startsWith('http'));
86
+ const badge = $derived(getItemBadge(item));
87
+ const childrenBadge = $derived(aggregateBadges(item.children));
55
88
 
56
89
  const finalHref = $derived(isExternal ? item.href : (item.href ? formatLink(item.href) : undefined));
57
-
58
- // Track open state — auto-open if a child is active
90
+ // Auto-open the active branch once, then respect manual collapse/expand.
59
91
  let isOpen = $state(false);
60
- $effect(() => { if (childActive) isOpen = true; });
92
+ let touchedOpen = $state(false);
93
+
94
+ function setOpen(nextOpen: boolean) {
95
+ touchedOpen = true;
96
+ isOpen = nextOpen;
97
+ }
98
+
99
+ $effect(() => {
100
+ if (childActive && !touchedOpen && !isOpen) isOpen = true;
101
+ });
61
102
 
62
103
  const Icon = $derived(getIcon(item.icon));
63
104
  const label = $derived(getLabel(item));
@@ -67,22 +108,33 @@
67
108
  <!-- hidden item, render nothing -->
68
109
  {:else if hasChildren && !collapsed}
69
110
  <!-- Parent node with children: render as collapsible group -->
70
- <Collapsible.Root bind:open={isOpen}>
111
+ <Collapsible.Root open={isOpen} onOpenChange={setOpen}>
71
112
  <Collapsible.Trigger
72
- class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
113
+ class="flex w-full items-center justify-between rounded-xl px-3 {depth === 1 ? 'py-1.5' : 'py-2'} text-sm font-medium transition-all duration-200
73
114
  {childActive
74
- ? 'text-sidebar-foreground bg-sidebar-accent/30'
75
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
76
- style="padding-left: {12 + depth * 12}px"
115
+ ? 'text-primary font-semibold'
116
+ : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
117
+ style="padding-left: {depth === 0 ? 12 : 4 + (depth - 1) * 12}px"
118
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
119
+ data-active={childActive ? "true" : "false"}
77
120
  >
78
121
  <span class="flex items-center gap-3">
79
122
  <Icon class="h-4 w-4 flex-shrink-0" />
80
123
  <span>{label}</span>
81
124
  </span>
82
- <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
125
+ <span class="ml-2 flex items-center gap-2">
126
+ {#if childrenBadge}
127
+ <span class="rounded-full {childActive ? 'bg-primary/12 text-primary' : 'bg-sidebar-accent text-sidebar-foreground/60'} px-2 py-0.5 text-[10px] font-semibold">{childrenBadge}</span>
128
+ {/if}
129
+ <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
130
+ </span>
83
131
  </Collapsible.Trigger>
84
132
  <Collapsible.Content>
85
- <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
133
+ <div
134
+ class="mt-0.5 {depth >= 1 ? 'divide-y divide-border/45' : 'space-y-0'}"
135
+ style="padding-left: {depth > 0 ? 0 : 0}px"
136
+ data-sidebar="menu-sub"
137
+ >
86
138
  {#each item.children as child, _i (_i)}
87
139
  <SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
88
140
  {/each}
@@ -99,10 +151,12 @@
99
151
  href={finalHref}
100
152
  target={isExternal ? '_blank' : undefined}
101
153
  rel={isExternal ? 'noopener noreferrer' : undefined}
102
- class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
154
+ class="mx-auto flex h-10 w-10 items-center justify-center text-sm font-medium transition-colors duration-200
103
155
  {active
104
- ? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5'
105
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
156
+ ? 'text-primary'
157
+ : 'text-sidebar-foreground/68 hover:text-sidebar-foreground'}"
158
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
159
+ data-active={active ? "true" : "false"}
106
160
  >
107
161
  <Icon class="h-4 w-4 flex-shrink-0" />
108
162
  </a>
@@ -118,16 +172,21 @@
118
172
  href={finalHref}
119
173
  target={isExternal ? '_blank' : undefined}
120
174
  rel={isExternal ? 'noopener noreferrer' : undefined}
121
- class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
175
+ class="relative flex items-center gap-3 px-3 {depth === 1 ? 'py-1.5' : 'py-2'} text-sm font-medium transition-all duration-200 {depth >= 2 ? 'first:rounded-t-xl last:rounded-b-xl' : 'rounded-xl'}
122
176
  {active
123
- ? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5 font-semibold before:absolute before:left-0 before:top-2 before:bottom-2 before:w-[3px] before:bg-primary before:rounded-r-md overflow-hidden relative'
124
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
177
+ ? 'text-primary font-semibold'
178
+ : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
125
179
  style="padding-left: {12 + depth * 12}px"
180
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
181
+ data-active={active ? "true" : "false"}
126
182
  >
127
183
  <Icon class="h-4 w-4 flex-shrink-0" />
128
184
  <span class="flex-1">{label}</span>
129
185
  {#if isExternal}
130
186
  <ExternalLink class="h-3 w-3 opacity-50" />
131
187
  {/if}
188
+ {#if badge}
189
+ <span class="rounded-full {active ? 'bg-primary/12 text-primary' : 'bg-sidebar-accent text-sidebar-foreground/60'} px-2 py-0.5 text-[10px] font-semibold">{badge}</span>
190
+ {/if}
132
191
  </a>
133
192
  {/if}
@@ -0,0 +1,247 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import * as Card from './ui/card/index.js';
4
+ import { Button } from './ui/button/index.js';
5
+ import { Input } from './ui/input/index.js';
6
+ import * as Alert from './ui/alert/index.js';
7
+ import { Badge } from './ui/badge/index.js';
8
+ import { Separator } from './ui/separator/index.js';
9
+ import { Shield, Copy, CheckCircle2, AlertTriangle, ArrowRight, ArrowLeft } from '@lucide/svelte';
10
+
11
+ type Step = 'intro' | 'scan' | 'verify' | 'recovery';
12
+
13
+ let currentStep = $state<Step>('intro');
14
+ let is2faEnabled = $state(false);
15
+ let verifyError = $state('');
16
+ let copiedRecovery = $state(false);
17
+ let codeDigits = $state<string[]>(Array.from({ length: 6 }, () => ''));
18
+
19
+ const verificationCode = $derived(codeDigits.join(''));
20
+ const qrCells = [
21
+ '11111110',
22
+ '10010010',
23
+ '10111010',
24
+ '10100100',
25
+ '11101010',
26
+ '00101110',
27
+ '11010100',
28
+ '01110110',
29
+ ].flatMap(row => [...row].map(cell => cell === '1'));
30
+
31
+ const recoveryCodes = [
32
+ 'A1B2-C3D4', 'E5F6-G7H8', 'I9J0-K1L2',
33
+ 'M3N4-O5P6', 'Q7R8-S9T0', 'U1V2-W3X4',
34
+ 'Y5Z6-A7B8', 'C9D0-E1F2',
35
+ ];
36
+
37
+ function startSetup() {
38
+ currentStep = 'scan';
39
+ }
40
+
41
+ function proceedToVerify() {
42
+ currentStep = 'verify';
43
+ }
44
+
45
+ function handleVerify() {
46
+ verifyError = '';
47
+ if (verificationCode.length !== 6) {
48
+ verifyError = t('auth.twoFactorEnterCode');
49
+ return;
50
+ }
51
+ currentStep = 'recovery';
52
+ }
53
+
54
+ function handleDigitInput(e: Event, index: number) {
55
+ const input = e.currentTarget as HTMLInputElement;
56
+ const digit = input.value.replace(/\D/g, '').slice(-1);
57
+ codeDigits[index] = digit;
58
+ input.value = digit;
59
+ if (digit && index < codeDigits.length - 1) {
60
+ const next = input.parentElement?.children[index + 1] as HTMLInputElement | undefined;
61
+ next?.focus();
62
+ }
63
+ }
64
+
65
+ function completeSetup() {
66
+ is2faEnabled = true;
67
+ currentStep = 'intro';
68
+ }
69
+
70
+ function disable2fa() {
71
+ is2faEnabled = false;
72
+ currentStep = 'intro';
73
+ }
74
+
75
+ function copyRecoveryCodes() {
76
+ navigator.clipboard?.writeText(recoveryCodes.join('\n'));
77
+ copiedRecovery = true;
78
+ setTimeout(() => { copiedRecovery = false; }, 2000);
79
+ }
80
+ </script>
81
+
82
+ <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="auth-2fa">
83
+ <div class="w-full max-w-lg space-y-6">
84
+ {#if currentStep === 'intro' && !is2faEnabled}
85
+ <!-- Intro -->
86
+ <Card.Card class="border-border/60">
87
+ <Card.CardContent class="space-y-6 p-8 text-center">
88
+ <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500">
89
+ <Shield class="h-8 w-8" />
90
+ </div>
91
+ <div class="space-y-2">
92
+ <h2 class="text-2xl font-semibold text-foreground">{t('auth.twoFactorSetup')}</h2>
93
+ <p class="text-sm text-muted-foreground">{t('security.twoFactorDescription')}</p>
94
+ </div>
95
+ <div class="space-y-3 text-left">
96
+ <div class="flex items-start gap-3 rounded-xl border p-3">
97
+ <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">1</div>
98
+ <div>
99
+ <p class="text-sm font-medium text-foreground">{t('auth.twoFactorScanQR')}</p>
100
+ <p class="text-xs text-muted-foreground">Google Authenticator, Authy, etc.</p>
101
+ </div>
102
+ </div>
103
+ <div class="flex items-start gap-3 rounded-xl border p-3">
104
+ <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">2</div>
105
+ <div>
106
+ <p class="text-sm font-medium text-foreground">{t('auth.twoFactorEnterCode')}</p>
107
+ <p class="text-xs text-muted-foreground">6-digit code from your authenticator</p>
108
+ </div>
109
+ </div>
110
+ <div class="flex items-start gap-3 rounded-xl border p-3">
111
+ <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">3</div>
112
+ <div>
113
+ <p class="text-sm font-medium text-foreground">{t('auth.twoFactorSaveRecovery')}</p>
114
+ <p class="text-xs text-muted-foreground">{t('auth.twoFactorRecoveryHint')}</p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <Button class="w-full" onclick={startSetup}>
119
+ {t('auth.twoFactorSetup')} <ArrowRight class="h-4 w-4 ml-2" />
120
+ </Button>
121
+ </Card.CardContent>
122
+ </Card.Card>
123
+
124
+ {:else if currentStep === 'scan'}
125
+ <!-- QR Code -->
126
+ <Card.Card class="border-border/60">
127
+ <Card.CardContent class="space-y-6 p-8">
128
+ <div class="text-center space-y-2">
129
+ <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorScanQR')}</h2>
130
+ <p class="text-sm text-muted-foreground">Use your authenticator app to scan the code below.</p>
131
+ </div>
132
+ <div class="mx-auto flex h-48 w-48 items-center justify-center rounded-xl border-2 border-dashed border-border/60 bg-muted/20">
133
+ <div class="grid grid-cols-8 gap-[2px]">
134
+ {#each qrCells as filled, i (i)}
135
+ <div class="h-3 w-3 rounded-[1px] {filled ? 'bg-foreground' : 'bg-transparent'}"></div>
136
+ {/each}
137
+ </div>
138
+ </div>
139
+ <div class="flex gap-3">
140
+ <Button variant="outline" class="flex-1" onclick={() => currentStep = 'intro'}>
141
+ <ArrowLeft class="h-4 w-4 mr-2" />{t('common.back')}
142
+ </Button>
143
+ <Button class="flex-1" onclick={proceedToVerify}>
144
+ {t('common.next')} <ArrowRight class="h-4 w-4 ml-2" />
145
+ </Button>
146
+ </div>
147
+ </Card.CardContent>
148
+ </Card.Card>
149
+
150
+ {:else if currentStep === 'verify'}
151
+ <!-- Verify -->
152
+ <Card.Card class="border-border/60">
153
+ <Card.CardContent class="space-y-6 p-8">
154
+ <div class="text-center space-y-2">
155
+ <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorVerify')}</h2>
156
+ <p class="text-sm text-muted-foreground">{t('auth.twoFactorEnterCode')}</p>
157
+ </div>
158
+
159
+ {#if verifyError}
160
+ <Alert.Root variant="destructive">
161
+ <AlertTriangle class="h-4 w-4" />
162
+ <Alert.Description>{verifyError}</Alert.Description>
163
+ </Alert.Root>
164
+ {/if}
165
+
166
+ <div class="flex justify-center gap-2">
167
+ {#each Array(6) as _, i (i)}
168
+ <Input
169
+ class="h-14 w-12 text-center text-xl font-bold"
170
+ maxlength={1}
171
+ inputmode="numeric"
172
+ pattern="[0-9]*"
173
+ bind:value={codeDigits[i]}
174
+ oninput={(e: Event) => handleDigitInput(e, i)}
175
+ />
176
+ {/each}
177
+ </div>
178
+
179
+ <div class="flex gap-3">
180
+ <Button variant="outline" class="flex-1" onclick={() => currentStep = 'scan'}>
181
+ <ArrowLeft class="h-4 w-4 mr-2" />{t('common.back')}
182
+ </Button>
183
+ <Button class="flex-1" onclick={handleVerify}>
184
+ {t('common.confirm')} <ArrowRight class="h-4 w-4 ml-2" />
185
+ </Button>
186
+ </div>
187
+ </Card.CardContent>
188
+ </Card.Card>
189
+
190
+ {:else if currentStep === 'recovery'}
191
+ <!-- Recovery codes -->
192
+ <Card.Card class="border-border/60">
193
+ <Card.CardContent class="space-y-6 p-8">
194
+ <div class="text-center space-y-2">
195
+ <div class="mx-auto inline-flex h-12 w-12 items-center justify-center rounded-xl bg-green-500/10 text-green-500">
196
+ <CheckCircle2 class="h-6 w-6" />
197
+ </div>
198
+ <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorRecovery')}</h2>
199
+ <p class="text-sm text-muted-foreground">{t('auth.twoFactorRecoveryHint')}</p>
200
+ </div>
201
+
202
+ <div class="rounded-xl border bg-muted/20 p-4">
203
+ <div class="grid grid-cols-2 gap-2 font-mono text-sm">
204
+ {#each recoveryCodes as code, i (i)}
205
+ <div class="rounded-lg bg-background px-3 py-1.5 text-center text-foreground">{code}</div>
206
+ {/each}
207
+ </div>
208
+ </div>
209
+
210
+ <div class="flex gap-3">
211
+ <Button variant="outline" class="flex-1" onclick={copyRecoveryCodes}>
212
+ {#if copiedRecovery}
213
+ <CheckCircle2 class="h-4 w-4 mr-2" />{t('common.copied')}
214
+ {:else}
215
+ <Copy class="h-4 w-4 mr-2" />{t('common.copyAll')}
216
+ {/if}
217
+ </Button>
218
+ <Button class="flex-1" onclick={completeSetup}>
219
+ {t('account.completeSetup')} <ArrowRight class="h-4 w-4 ml-2" />
220
+ </Button>
221
+ </div>
222
+ </Card.CardContent>
223
+ </Card.Card>
224
+
225
+ {:else}
226
+ <!-- 2FA enabled state -->
227
+ <Card.Card class="border-border/60">
228
+ <Card.CardContent class="space-y-6 p-8 text-center">
229
+ <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-green-500/10 text-green-500">
230
+ <Shield class="h-8 w-8" />
231
+ </div>
232
+ <div class="space-y-2">
233
+ <h2 class="text-2xl font-semibold text-foreground">{t('auth.twoFactorVerifySuccess')}</h2>
234
+ <p class="text-sm text-muted-foreground">{t('security.twoFactorActive')}</p>
235
+ </div>
236
+ <Badge variant="secondary" class="bg-green-500/10 text-green-600">
237
+ <CheckCircle2 class="h-3 w-3 mr-1" />2FA Active
238
+ </Badge>
239
+ <Separator />
240
+ <Button variant="destructive" onclick={disable2fa}>
241
+ {t('auth.twoFactorDisable')}
242
+ </Button>
243
+ </Card.CardContent>
244
+ </Card.Card>
245
+ {/if}
246
+ </div>
247
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const TwoFactorAuthPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TwoFactorAuthPage = ReturnType<typeof TwoFactorAuthPage>;
3
+ export default TwoFactorAuthPage;
@@ -3,10 +3,9 @@
3
3
  import { t } from '@svadmin/core/i18n';
4
4
  import { navigate } from '@svadmin/core/router';
5
5
  import { Button } from './ui/button/index.js';
6
- import * as Card from './ui/card/index.js';
7
6
  import * as Alert from './ui/alert/index.js';
8
7
  import PasswordInput from './PasswordInput.svelte';
9
- import { ShieldCheck, Loader2, AlertCircle } from '@lucide/svelte';
8
+ import { ShieldCheck, Loader2, AlertCircle, Shield } from '@lucide/svelte';
10
9
 
11
10
  let { title = 'Admin' } = $props<{
12
11
  title?: string;
@@ -32,58 +31,97 @@
32
31
  }
33
32
  </script>
34
33
 
35
- <div class="min-h-screen flex items-center justify-center bg-background p-4">
36
- <div class="w-full max-w-[380px]">
37
- <Card.Card>
38
- <Card.CardHeader class="text-center pb-2">
39
- <div class="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 text-primary mx-auto mb-3">
34
+ <div class="min-h-screen flex flex-col lg:flex-row">
35
+ <!-- Left brand panel -->
36
+ <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">
37
+ <div class="absolute inset-0">
38
+ <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>
39
+ <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>
40
+ <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>
41
+ <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>
42
+ <div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
43
+ </div>
44
+
45
+ <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
46
+ <div class="flex items-center gap-3">
47
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 backdrop-blur-sm">
48
+ <Shield class="h-5 w-5 text-white" />
49
+ </div>
50
+ <span class="text-lg font-semibold text-white tracking-tight">{title}</span>
51
+ </div>
52
+
53
+ <div class="space-y-4">
54
+ <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
55
+ {t('auth.resetPassword')}
56
+ </h2>
57
+ <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
58
+ {t('auth.resetPasswordDescription')}
59
+ </p>
60
+ </div>
61
+
62
+ <div class="flex items-center gap-3">
63
+ <div class="h-1 w-8 rounded-full bg-white/30"></div>
64
+ <div class="h-1 w-4 rounded-full bg-white/15"></div>
65
+ <div class="h-1 w-2 rounded-full bg-white/10"></div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Right form panel -->
71
+ <div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
72
+ <div class="w-full max-w-[400px]">
73
+ <div class="flex items-center gap-3 mb-8 lg:hidden">
74
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
40
75
  <ShieldCheck class="h-5 w-5" />
41
76
  </div>
42
- <Card.CardTitle class="text-xl">{t('auth.resetPassword')}</Card.CardTitle>
77
+ <span class="text-lg font-semibold tracking-tight">{title}</span>
78
+ </div>
79
+
80
+ <div class="space-y-1 mb-8">
81
+ <h1 class="text-2xl font-semibold tracking-tight">{t('auth.resetPassword')}</h1>
43
82
  <p class="text-sm text-muted-foreground">{t('auth.resetPasswordDescription')}</p>
44
- </Card.CardHeader>
45
- <Card.CardContent>
46
- <form onsubmit={handleSubmit} class="space-y-4">
47
- {#if error}
48
- <Alert.Root variant="destructive">
49
- <AlertCircle class="h-4 w-4" />
50
- <Alert.Description>{error}</Alert.Description>
51
- </Alert.Root>
83
+ </div>
84
+
85
+ <form onsubmit={handleSubmit} class="space-y-5">
86
+ {#if error}
87
+ <Alert.Root variant="destructive">
88
+ <AlertCircle class="h-4 w-4" />
89
+ <Alert.Description>{error}</Alert.Description>
90
+ </Alert.Root>
91
+ {/if}
92
+
93
+ <PasswordInput
94
+ id="new-password"
95
+ label={t('auth.password')}
96
+ bind:value={password}
97
+ autocomplete="new-password"
98
+ showStrength
99
+ />
100
+
101
+ <PasswordInput
102
+ id="confirm-password"
103
+ label={t('auth.confirmPassword')}
104
+ bind:value={confirmPassword}
105
+ autocomplete="new-password"
106
+ />
107
+
108
+ <Button type="submit" class="w-full h-10" disabled={updatePw.isLoading}>
109
+ {#if updatePw.isLoading}
110
+ <Loader2 class="h-4 w-4 animate-spin mr-2" />
52
111
  {/if}
112
+ {t('auth.resetPassword')}
113
+ </Button>
53
114
 
54
- <PasswordInput
55
- id="new-password"
56
- label={t('auth.password')}
57
- bind:value={password}
58
- autocomplete="new-password"
59
- showStrength
60
- />
61
-
62
- <PasswordInput
63
- id="confirm-password"
64
- label={t('auth.confirmPassword')}
65
- bind:value={confirmPassword}
66
- autocomplete="new-password"
67
- />
68
-
69
- <Button type="submit" class="w-full" disabled={updatePw.isLoading}>
70
- {#if updatePw.isLoading}
71
- <Loader2 class="h-4 w-4 animate-spin mr-2" />
72
- {/if}
73
- {t('auth.resetPassword')}
115
+ <div class="flex items-center justify-center mt-2">
116
+ <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
117
+ {t('auth.backToLogin')}
74
118
  </Button>
119
+ </div>
120
+ </form>
75
121
 
76
- <div class="flex items-center justify-center mt-2">
77
- <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
78
- {t('auth.backToLogin')}
79
- </Button>
80
- </div>
81
- </form>
82
- </Card.CardContent>
83
- </Card.Card>
84
-
85
- <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
86
- Powered by {title}
87
- </p>
122
+ <p class="text-xs text-muted-foreground/60 mt-8 text-center">
123
+ Powered by {title}
124
+ </p>
125
+ </div>
88
126
  </div>
89
127
  </div>