@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.
- package/dist/app.css +746 -22
- package/dist/component-registry.svelte.d.ts +1 -0
- package/dist/components/AdminApp.svelte +94 -15
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AutoForm.svelte +47 -42
- package/dist/components/AutoTable.svelte +29 -24
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +2 -2
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +6 -7
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/Layout.svelte +7 -7
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/NotificationsSettings.svelte +115 -0
- package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +9 -7
- package/dist/components/RegisterPage.svelte +103 -65
- package/dist/components/ResourceOperationsPage.svelte +151 -0
- package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +120 -87
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +278 -189
- package/dist/components/SidebarItem.svelte +77 -18
- package/dist/components/TwoFactorAuthPage.svelte +247 -0
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
- package/dist/components/UpdatePasswordPage.svelte +86 -48
- package/dist/components/account/CompanyProfilePage.svelte +99 -0
- package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
- package/dist/components/account/GetStartedPage.svelte +123 -0
- package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
- package/dist/components/account/ImportMembersPage.svelte +143 -0
- package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
- package/dist/components/account/MembersStarterPage.svelte +104 -0
- package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
- package/dist/components/account/SecurityLogPage.svelte +135 -0
- package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
- package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsPlainPage.svelte +81 -0
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsSidebarPage.svelte +98 -0
- package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
- package/dist/components/account/TeamMembersPage.svelte +98 -0
- package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
- package/dist/components/network/TeamCrewTablePage.svelte +155 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/router-state.svelte.js +17 -0
- 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,
|
|
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 |
|
|
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
|
-
|
|
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
|
|
111
|
+
<Collapsible.Root open={isOpen} onOpenChange={setOpen}>
|
|
71
112
|
<Collapsible.Trigger
|
|
72
|
-
class="flex w-full items-center justify-between rounded-
|
|
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-
|
|
75
|
-
: '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
|
-
<
|
|
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
|
|
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
|
|
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
|
-
? '
|
|
105
|
-
: '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
|
|
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
|
-
? '
|
|
124
|
-
: '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>
|
|
@@ -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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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>
|