@svadmin/ui 0.34.10 → 0.35.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 +372 -0
- 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 +48 -43
- package/dist/components/AutoTable.svelte +27 -22
- 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 +8 -8
- 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 +153 -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 +121 -54
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +96 -48
- package/dist/components/SidebarItem.svelte +37 -12
- 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 +108 -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 +122 -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/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;
|
|
@@ -69,11 +85,13 @@
|
|
|
69
85
|
<!-- Parent node with children: render as collapsible group -->
|
|
70
86
|
<Collapsible.Root bind:open={isOpen}>
|
|
71
87
|
<Collapsible.Trigger
|
|
72
|
-
class="flex w-full items-center justify-between rounded-
|
|
88
|
+
class="flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
|
|
73
89
|
{childActive
|
|
74
|
-
? 'text-
|
|
75
|
-
: 'text-sidebar-foreground/
|
|
90
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/15'
|
|
91
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
|
|
76
92
|
style="padding-left: {12 + depth * 12}px"
|
|
93
|
+
data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
|
|
94
|
+
data-active={childActive ? "true" : "false"}
|
|
77
95
|
>
|
|
78
96
|
<span class="flex items-center gap-3">
|
|
79
97
|
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -82,7 +100,7 @@
|
|
|
82
100
|
<ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
|
|
83
101
|
</Collapsible.Trigger>
|
|
84
102
|
<Collapsible.Content>
|
|
85
|
-
<div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
|
|
103
|
+
<div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px" data-sidebar="menu-sub">
|
|
86
104
|
{#each item.children as child, _i (_i)}
|
|
87
105
|
<SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
|
|
88
106
|
{/each}
|
|
@@ -99,10 +117,12 @@
|
|
|
99
117
|
href={finalHref}
|
|
100
118
|
target={isExternal ? '_blank' : undefined}
|
|
101
119
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
102
|
-
class="flex items-center justify-center rounded-
|
|
120
|
+
class="flex items-center justify-center rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
103
121
|
{active
|
|
104
|
-
? 'bg-
|
|
105
|
-
: 'text-sidebar-foreground/
|
|
122
|
+
? 'bg-primary/10 text-primary shadow-sm shadow-primary/10 ring-1 ring-primary/15'
|
|
123
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
|
|
124
|
+
data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
|
|
125
|
+
data-active={active ? "true" : "false"}
|
|
106
126
|
>
|
|
107
127
|
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
108
128
|
</a>
|
|
@@ -118,12 +138,17 @@
|
|
|
118
138
|
href={finalHref}
|
|
119
139
|
target={isExternal ? '_blank' : undefined}
|
|
120
140
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
121
|
-
class="flex items-center gap-3 rounded-
|
|
141
|
+
class="relative flex items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
|
|
122
142
|
{active
|
|
123
|
-
? 'bg-
|
|
124
|
-
: 'text-sidebar-foreground/
|
|
143
|
+
? 'bg-primary/10 text-primary font-semibold shadow-sm shadow-primary/10 ring-1 ring-primary/15'
|
|
144
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
|
|
125
145
|
style="padding-left: {12 + depth * 12}px"
|
|
146
|
+
data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
|
|
147
|
+
data-active={active ? "true" : "false"}
|
|
126
148
|
>
|
|
149
|
+
{#if active && !collapsed}
|
|
150
|
+
<div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
|
|
151
|
+
{/if}
|
|
127
152
|
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
128
153
|
<span class="flex-1">{label}</span>
|
|
129
154
|
{#if isExternal}
|
|
@@ -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">
|
|
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>
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { Label } from '../ui/label/index.js';
|
|
7
|
+
import { Upload, Loader2 } from '@lucide/svelte';
|
|
8
|
+
|
|
9
|
+
let companyName = $state('Acme Corporation');
|
|
10
|
+
let industry = $state('Technology');
|
|
11
|
+
let website = $state('https://acme.com');
|
|
12
|
+
let employeeCount = $state('1,250');
|
|
13
|
+
let foundedYear = $state('2015');
|
|
14
|
+
let description = $state('Building the future of enterprise software with cutting-edge AI and cloud solutions.');
|
|
15
|
+
let saving = $state(false);
|
|
16
|
+
|
|
17
|
+
async function handleSave() {
|
|
18
|
+
saving = true;
|
|
19
|
+
await new Promise(r => setTimeout(r, 800));
|
|
20
|
+
saving = false;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="space-y-6">
|
|
25
|
+
<div>
|
|
26
|
+
<h2 class="text-xl font-semibold text-foreground">{t('account.companyProfile')}</h2>
|
|
27
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('account.companyProfileDescription')}</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Company Avatar & Banner -->
|
|
31
|
+
<Card.Card class="border-border/60">
|
|
32
|
+
<Card.CardContent class="p-0">
|
|
33
|
+
<div class="h-32 bg-gradient-to-r from-primary/20 via-primary/10 to-accent/20 relative rounded-t-xl">
|
|
34
|
+
<button class="absolute bottom-2 right-2 flex h-8 w-8 items-center justify-center rounded-lg bg-black/40 text-white hover:bg-black/60 transition-colors">
|
|
35
|
+
<Upload class="h-4 w-4" />
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="relative px-6 pb-5">
|
|
39
|
+
<div class="-mt-8 flex items-end gap-4">
|
|
40
|
+
<div class="ring-4 ring-card rounded-xl overflow-hidden shrink-0">
|
|
41
|
+
<div class="flex h-16 w-16 items-center justify-center bg-primary/10 text-primary text-xl font-bold">
|
|
42
|
+
{companyName.split(' ').map(n => n[0]).join('').slice(0, 2)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="pb-1">
|
|
46
|
+
<h3 class="text-lg font-semibold text-foreground">{companyName}</h3>
|
|
47
|
+
<p class="text-sm text-muted-foreground">{industry}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</Card.CardContent>
|
|
52
|
+
</Card.Card>
|
|
53
|
+
|
|
54
|
+
<!-- Company Details Form -->
|
|
55
|
+
<Card.Card class="border-border/60">
|
|
56
|
+
<Card.CardHeader>
|
|
57
|
+
<Card.CardTitle class="text-base">{t('profile.companyName')}</Card.CardTitle>
|
|
58
|
+
</Card.CardHeader>
|
|
59
|
+
<Card.CardContent class="space-y-4">
|
|
60
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
61
|
+
<div class="space-y-2">
|
|
62
|
+
<Label for="company-name">{t('profile.companyName')}</Label>
|
|
63
|
+
<Input id="company-name" bind:value={companyName} />
|
|
64
|
+
</div>
|
|
65
|
+
<div class="space-y-2">
|
|
66
|
+
<Label for="company-industry">{t('profile.industry')}</Label>
|
|
67
|
+
<Input id="company-industry" bind:value={industry} />
|
|
68
|
+
</div>
|
|
69
|
+
<div class="space-y-2">
|
|
70
|
+
<Label for="company-website">{t('profile.website')}</Label>
|
|
71
|
+
<Input id="company-website" bind:value={website} type="url" />
|
|
72
|
+
</div>
|
|
73
|
+
<div class="space-y-2">
|
|
74
|
+
<Label for="company-employees">{t('profile.employees')}</Label>
|
|
75
|
+
<Input id="company-employees" bind:value={employeeCount} />
|
|
76
|
+
</div>
|
|
77
|
+
<div class="space-y-2">
|
|
78
|
+
<Label for="company-founded">{t('profile.founded')}</Label>
|
|
79
|
+
<Input id="company-founded" bind:value={foundedYear} />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<Label for="company-description">{t('profile.companyDescription')}</Label>
|
|
84
|
+
<textarea
|
|
85
|
+
id="company-description"
|
|
86
|
+
bind:value={description}
|
|
87
|
+
rows={3}
|
|
88
|
+
class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
89
|
+
></textarea>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="flex justify-end">
|
|
92
|
+
<Button onclick={handleSave} disabled={saving}>
|
|
93
|
+
{#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
|
|
94
|
+
{t('common.save')}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
</Card.CardContent>
|
|
98
|
+
</Card.Card>
|
|
99
|
+
</div>
|