@svadmin/ui 0.42.3 → 0.43.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 +462 -825
- package/dist/app.theme.css +462 -825
- package/dist/components/ErrorPage.svelte +4 -56
- package/dist/components/TwoFactorAuthPage.svelte +44 -232
- package/dist/components/account/CompanyProfilePage.svelte +21 -142
- package/dist/components/account/GetStartedPage.svelte +33 -115
- package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
- package/dist/components/account/SecurityLogPage.svelte +16 -131
- package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
- package/dist/components/account/SettingsPlainPage.svelte +10 -175
- package/dist/components/account/SettingsSidebarPage.svelte +11 -304
- package/dist/components/account/TeamMembersPage.svelte +18 -187
- package/dist/components/account/UserProfilePage.svelte +19 -196
- package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
- package/dist/components/content/ApiKeyList.svelte +13 -0
- package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
- package/dist/components/content/ContentPageHeader.svelte +31 -0
- package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
- package/dist/components/content/ContentPageShell.svelte +39 -0
- package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
- package/dist/components/content/DataState.svelte +44 -0
- package/dist/components/content/DataState.svelte.d.ts +13 -0
- package/dist/components/content/DescriptionList.svelte +27 -0
- package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
- package/dist/components/content/FileList.svelte +13 -0
- package/dist/components/content/FileList.svelte.d.ts +16 -0
- package/dist/components/content/FilterToolbar.svelte +24 -0
- package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
- package/dist/components/content/ImportDropzone.svelte +19 -0
- package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
- package/dist/components/content/IntegrationCard.svelte +16 -0
- package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
- package/dist/components/content/MemberList.svelte +16 -0
- package/dist/components/content/MemberList.svelte.d.ts +17 -0
- package/dist/components/content/MetricBlock.svelte +27 -0
- package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
- package/dist/components/content/NetworkTable.svelte +18 -0
- package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
- package/dist/components/content/NetworkTable.types.d.ts +4 -0
- package/dist/components/content/NetworkTable.types.js +1 -0
- package/dist/components/content/NetworkUserCard.svelte +17 -0
- package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
- package/dist/components/content/OtpInput.svelte +22 -0
- package/dist/components/content/OtpInput.svelte.d.ts +10 -0
- package/dist/components/content/PageToolbar.svelte +18 -0
- package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
- package/dist/components/content/ProjectCard.svelte +39 -0
- package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/content/SectionHeader.svelte +18 -0
- package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
- package/dist/components/content/SecurityEventTable.svelte +18 -0
- package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
- package/dist/components/content/StatusBadge.svelte +19 -0
- package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
- package/dist/components/content/SystemErrorState.svelte +18 -0
- package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
- package/dist/components/content/TeamCard.svelte +26 -0
- package/dist/components/content/TeamCard.svelte.d.ts +23 -0
- package/dist/components/content/TwoFactorStepper.svelte +11 -0
- package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
- package/dist/components/content/index.d.ts +32 -0
- package/dist/components/content/index.js +22 -0
- package/dist/components/network/TeamCrewTablePage.svelte +29 -181
- package/dist/components/network/UserCardsNFTPage.svelte +24 -137
- package/dist/components/profile/ActivityTimeline.svelte +17 -91
- package/dist/components/profile/ProfileCard.svelte +4 -4
- package/dist/components/profile/ProjectsGrid.svelte +28 -106
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
- package/dist/components/profile/PublicProfilePage.svelte +31 -181
- package/dist/components/profile/TeamsShowcase.svelte +12 -114
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
- package/dist/index.d.ts +36 -0
- package/dist/index.js +24 -0
- package/dist/reference-data.d.ts +171 -0
- package/dist/reference-data.js +39 -0
- package/package.json +1 -1
|
@@ -1,60 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import { AlertTriangle, ArrowLeft, FileQuestion, Home } from '@lucide/svelte';
|
|
6
|
-
import { Button } from './ui/button/index.js';
|
|
7
|
-
import * as Card from './ui/card/index.js';
|
|
8
|
-
|
|
9
|
-
const i18n = useTranslation();
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
status?: '404' | '500';
|
|
13
|
-
title?: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
2
|
+
import SystemErrorState from './content/SystemErrorState.svelte';
|
|
3
|
+
import ContentPageShell from './content/ContentPageShell.svelte';
|
|
4
|
+
interface Props { status?: '404' | '500'; title?: string; description?: string; }
|
|
17
5
|
let { status = '404', title, description }: Props = $props();
|
|
18
|
-
const adminContext = captureAdminContext();
|
|
19
|
-
|
|
20
|
-
function goBack() {
|
|
21
|
-
adminContext.back();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const isNotFound = $derived(status === '404');
|
|
25
|
-
const resolvedTitle = $derived(title ?? (isNotFound ? i18n.t('common.pageNotFound') : i18n.t('common.error')));
|
|
26
|
-
const resolvedDescription = $derived(
|
|
27
|
-
description ?? (isNotFound ? i18n.t('error.pageNotFoundDescription') : i18n.t('error.internalServerErrorDescription')),
|
|
28
|
-
);
|
|
29
6
|
</script>
|
|
30
7
|
|
|
31
|
-
<
|
|
32
|
-
<Card.Card class="w-full max-w-md border-border/60">
|
|
33
|
-
<Card.CardContent class="space-y-6 px-6 py-10 text-center">
|
|
34
|
-
<div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-lg bg-muted/50 text-muted-foreground">
|
|
35
|
-
{#if isNotFound}
|
|
36
|
-
<FileQuestion class="h-8 w-8" />
|
|
37
|
-
{:else}
|
|
38
|
-
<AlertTriangle class="h-8 w-8 text-destructive" />
|
|
39
|
-
{/if}
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div class="space-y-2">
|
|
43
|
-
<p class="text-xs font-semibold uppercase text-muted-foreground">{status}</p>
|
|
44
|
-
<h1 class="text-2xl font-semibold text-foreground">{resolvedTitle}</h1>
|
|
45
|
-
<p class="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">{resolvedDescription}</p>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div class="flex flex-col justify-center gap-2 sm:flex-row">
|
|
49
|
-
<Button variant="outline" onclick={goBack}>
|
|
50
|
-
<ArrowLeft class="h-4 w-4" />
|
|
51
|
-
{i18n.t('common.back')}
|
|
52
|
-
</Button>
|
|
53
|
-
<Button onclick={() => adminContext.navigate('/')}>
|
|
54
|
-
<Home class="h-4 w-4" />
|
|
55
|
-
{i18n.t('common.returnHome')}
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
</Card.CardContent>
|
|
59
|
-
</Card.Card>
|
|
60
|
-
</div>
|
|
8
|
+
<ContentPageShell pageId="system-error" width="narrow"><SystemErrorState {status} {title} {description} /></ContentPageShell>
|
|
@@ -1,249 +1,61 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import
|
|
3
|
+
import { ArrowLeft, ArrowRight, Check, CheckCircle2, Copy, Shield } from '@lucide/svelte';
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
|
-
import
|
|
5
|
+
import * as Card from './ui/card/index.js';
|
|
6
6
|
import * as Alert from './ui/alert/index.js';
|
|
7
|
+
import ContentPageShell from './content/ContentPageShell.svelte';
|
|
8
|
+
import ContentPageHeader from './content/ContentPageHeader.svelte';
|
|
9
|
+
import OtpInput from './content/OtpInput.svelte';
|
|
10
|
+
import TwoFactorStepper from './content/TwoFactorStepper.svelte';
|
|
7
11
|
import { Badge } from './ui/badge/index.js';
|
|
8
12
|
import { Separator } from './ui/separator/index.js';
|
|
9
|
-
import { Shield, Copy, CheckCircle2, AlertTriangle, ArrowRight, ArrowLeft } from '@lucide/svelte';
|
|
10
|
-
|
|
11
|
-
const i18n = useTranslation();
|
|
12
13
|
|
|
13
14
|
type Step = 'intro' | 'scan' | 'verify' | 'recovery';
|
|
14
|
-
|
|
15
|
+
const i18n = useTranslation();
|
|
15
16
|
let currentStep = $state<Step>('intro');
|
|
16
|
-
let
|
|
17
|
-
let verifyError = $state('');
|
|
18
|
-
let copiedRecovery = $state(false);
|
|
17
|
+
let enabled = $state(false);
|
|
19
18
|
let codeDigits = $state<string[]>(Array.from({ length: 6 }, () => ''));
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'10100100',
|
|
27
|
-
'11101010',
|
|
28
|
-
'00101110',
|
|
29
|
-
'11010100',
|
|
30
|
-
'01110110',
|
|
31
|
-
].flatMap(row => [...row].map(cell => cell === '1'));
|
|
32
|
-
|
|
33
|
-
const recoveryCodes = [
|
|
34
|
-
'A1B2-C3D4', 'E5F6-G7H8', 'I9J0-K1L2',
|
|
35
|
-
'M3N4-O5P6', 'Q7R8-S9T0', 'U1V2-W3X4',
|
|
36
|
-
'Y5Z6-A7B8', 'C9D0-E1F2',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
function startSetup() {
|
|
40
|
-
currentStep = 'scan';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function proceedToVerify() {
|
|
44
|
-
currentStep = 'verify';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function handleVerify() {
|
|
48
|
-
verifyError = '';
|
|
49
|
-
if (verificationCode.length !== 6) {
|
|
50
|
-
verifyError = i18n.t('auth.twoFactorEnterCode');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
currentStep = 'recovery';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function handleDigitInput(e: Event, index: number) {
|
|
57
|
-
const input = e.currentTarget as HTMLInputElement;
|
|
58
|
-
const digit = input.value.replace(/\D/g, '').slice(-1);
|
|
59
|
-
codeDigits[index] = digit;
|
|
60
|
-
input.value = digit;
|
|
61
|
-
if (digit && index < codeDigits.length - 1) {
|
|
62
|
-
const next = input.parentElement?.children[index + 1] as HTMLInputElement | undefined;
|
|
63
|
-
next?.focus();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function completeSetup() {
|
|
68
|
-
is2faEnabled = true;
|
|
69
|
-
currentStep = 'intro';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function disable2fa() {
|
|
73
|
-
is2faEnabled = false;
|
|
74
|
-
currentStep = 'intro';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function copyRecoveryCodes() {
|
|
78
|
-
navigator.clipboard?.writeText(recoveryCodes.join('\n'));
|
|
79
|
-
copiedRecovery = true;
|
|
80
|
-
setTimeout(() => { copiedRecovery = false; }, 2000);
|
|
81
|
-
}
|
|
19
|
+
let error = $state('');
|
|
20
|
+
let copied = $state(false);
|
|
21
|
+
const recoveryCodes = ['A1B2-C3D4', 'E5F6-G7H8', 'I9J0-K1L2', 'M3N4-O5P6', 'Q7R8-S9T0', 'U1V2-W3X4'];
|
|
22
|
+
const currentIndex = $derived(currentStep === 'intro' ? 0 : currentStep === 'scan' ? 1 : currentStep === 'verify' ? 2 : 3);
|
|
23
|
+
function verify() { if (codeDigits.join('').length !== 6) { error = i18n.t('auth.twoFactorEnterCode'); return; } error = ''; currentStep = 'recovery'; }
|
|
24
|
+
function copyCodes() { navigator.clipboard?.writeText(recoveryCodes.join('\n')); copied = true; setTimeout(() => copied = false, 1500); }
|
|
82
25
|
</script>
|
|
83
26
|
|
|
84
|
-
<
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</div>
|
|
93
|
-
<div class="space-y-2">
|
|
94
|
-
<h2 class="text-2xl font-semibold text-foreground">{i18n.t('auth.twoFactorSetup')}</h2>
|
|
95
|
-
<p class="text-sm text-muted-foreground">{i18n.t('security.twoFactorDescription')}</p>
|
|
96
|
-
</div>
|
|
97
|
-
<div class="space-y-3 text-left">
|
|
98
|
-
<div class="flex items-start gap-3 rounded-xl border p-3">
|
|
99
|
-
<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>
|
|
100
|
-
<div>
|
|
101
|
-
<p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorScanQR')}</p>
|
|
102
|
-
<p class="text-xs text-muted-foreground">Google Authenticator, Authy, etc.</p>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="flex items-start gap-3 rounded-xl border p-3">
|
|
106
|
-
<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>
|
|
107
|
-
<div>
|
|
108
|
-
<p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorEnterCode')}</p>
|
|
109
|
-
<p class="text-xs text-muted-foreground">6-digit code from your authenticator</p>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
<div class="flex items-start gap-3 rounded-xl border p-3">
|
|
113
|
-
<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>
|
|
114
|
-
<div>
|
|
115
|
-
<p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorSaveRecovery')}</p>
|
|
116
|
-
<p class="text-xs text-muted-foreground">{i18n.t('auth.twoFactorRecoveryHint')}</p>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
<Button class="w-full" onclick={startSetup}>
|
|
121
|
-
{i18n.t('auth.twoFactorSetup')} <ArrowRight class="h-4 w-4 ml-2" />
|
|
122
|
-
</Button>
|
|
123
|
-
</Card.CardContent>
|
|
124
|
-
</Card.Card>
|
|
125
|
-
|
|
27
|
+
<ContentPageShell pageId="auth-2fa" width="narrow">
|
|
28
|
+
<ContentPageHeader title={i18n.t('auth.twoFactorSetup')} description={i18n.t('security.twoFactorDescription')} />
|
|
29
|
+
{#if !enabled}<TwoFactorStepper current={currentIndex} steps={[i18n.t('auth.twoFactorSetup'), i18n.t('auth.twoFactorScanQR'), i18n.t('auth.twoFactorVerify'), i18n.t('auth.twoFactorRecovery')]} />{/if}
|
|
30
|
+
<Card.Card><Card.CardContent class="space-y-6 p-6">
|
|
31
|
+
{#if currentStep === 'intro' && !enabled}
|
|
32
|
+
<div class="flex items-start gap-3"><span class="flex size-10 items-center justify-center rounded-md bg-primary/10 text-primary"><Shield class="size-5" /></span><div><h2 class="text-base font-semibold text-foreground">{i18n.t('auth.twoFactorSetup')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('security.twoFactorDescription')}</p></div></div>
|
|
33
|
+
<ol class="space-y-2 text-sm text-muted-foreground"><li class="rounded-md border border-border p-3">1. {i18n.t('auth.twoFactorScanQR')}</li><li class="rounded-md border border-border p-3">2. {i18n.t('auth.twoFactorEnterCode')}</li><li class="rounded-md border border-border p-3">3. {i18n.t('auth.twoFactorSaveRecovery')}</li></ol>
|
|
34
|
+
<Button class="w-full" onclick={() => currentStep = 'scan'}>{i18n.t('common.next')}<ArrowRight class="size-4" /></Button>
|
|
126
35
|
{:else if currentStep === 'scan'}
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
<Card.CardContent class="space-y-6 p-8">
|
|
130
|
-
<div class="text-center space-y-2">
|
|
131
|
-
<h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorScanQR')}</h2>
|
|
132
|
-
<p class="text-sm text-muted-foreground">Use your authenticator app to scan the code below.</p>
|
|
133
|
-
</div>
|
|
134
|
-
<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">
|
|
135
|
-
<div class="grid grid-cols-8 gap-[2px]">
|
|
136
|
-
{#each qrCells as filled, i (i)}
|
|
137
|
-
<div class="h-3 w-3 rounded-[1px] {filled ? 'bg-foreground' : 'bg-transparent'}"></div>
|
|
138
|
-
{/each}
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="flex gap-3">
|
|
142
|
-
<Button variant="outline" class="flex-1" onclick={() => currentStep = 'intro'}>
|
|
143
|
-
<ArrowLeft class="h-4 w-4 mr-2" />{i18n.t('common.back')}
|
|
144
|
-
</Button>
|
|
145
|
-
<Button class="flex-1" onclick={proceedToVerify}>
|
|
146
|
-
{i18n.t('common.next')} <ArrowRight class="h-4 w-4 ml-2" />
|
|
147
|
-
</Button>
|
|
148
|
-
</div>
|
|
149
|
-
</Card.CardContent>
|
|
150
|
-
</Card.Card>
|
|
151
|
-
|
|
36
|
+
<div class="space-y-4 text-center"><div class="mx-auto grid size-48 grid-cols-8 gap-0.5 rounded-md border border-border bg-muted p-5">{#each Array(64) as _, index (index)}<span class={'aspect-square ' + ((index * 11 + index % 7) % 3 === 0 ? 'bg-foreground' : 'bg-transparent')}></span>{/each}</div><p class="text-sm text-muted-foreground">{i18n.t('auth.twoFactorScanQR')}</p></div>
|
|
37
|
+
<div class="flex justify-between gap-2"><Button variant="outline" onclick={() => currentStep = 'intro'}><ArrowLeft class="size-4" />{i18n.t('common.back')}</Button><Button onclick={() => currentStep = 'verify'}>{i18n.t('common.next')}<ArrowRight class="size-4" /></Button></div>
|
|
152
38
|
{:else if currentStep === 'verify'}
|
|
153
|
-
|
|
154
|
-
<Card.Card class="border-border/60">
|
|
155
|
-
<Card.CardContent class="space-y-6 p-8">
|
|
156
|
-
<div class="text-center space-y-2">
|
|
157
|
-
<h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorVerify')}</h2>
|
|
158
|
-
<p class="text-sm text-muted-foreground">{i18n.t('auth.twoFactorEnterCode')}</p>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
{#if verifyError}
|
|
162
|
-
<Alert.Root variant="destructive">
|
|
163
|
-
<AlertTriangle class="h-4 w-4" />
|
|
164
|
-
<Alert.Description>{verifyError}</Alert.Description>
|
|
165
|
-
</Alert.Root>
|
|
166
|
-
{/if}
|
|
167
|
-
|
|
168
|
-
<div class="flex justify-center gap-2">
|
|
169
|
-
{#each Array(6) as _, i (i)}
|
|
170
|
-
<Input
|
|
171
|
-
class="h-14 w-12 text-center text-xl font-bold"
|
|
172
|
-
maxlength={1}
|
|
173
|
-
inputmode="numeric"
|
|
174
|
-
pattern="[0-9]*"
|
|
175
|
-
bind:value={codeDigits[i]}
|
|
176
|
-
oninput={(e: Event) => handleDigitInput(e, i)}
|
|
177
|
-
/>
|
|
178
|
-
{/each}
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div class="flex gap-3">
|
|
182
|
-
<Button variant="outline" class="flex-1" onclick={() => currentStep = 'scan'}>
|
|
183
|
-
<ArrowLeft class="h-4 w-4 mr-2" />{i18n.t('common.back')}
|
|
184
|
-
</Button>
|
|
185
|
-
<Button class="flex-1" onclick={handleVerify}>
|
|
186
|
-
{i18n.t('common.confirm')} <ArrowRight class="h-4 w-4 ml-2" />
|
|
187
|
-
</Button>
|
|
188
|
-
</div>
|
|
189
|
-
</Card.CardContent>
|
|
190
|
-
</Card.Card>
|
|
191
|
-
|
|
39
|
+
<div class="space-y-4"><div><h2 class="text-base font-semibold text-foreground">{i18n.t('auth.twoFactorVerify')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('auth.twoFactorEnterCode')}</p></div>{#if error}<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>{/if}<OtpInput bind:value={codeDigits} /><div class="flex justify-between gap-2"><Button variant="outline" onclick={() => currentStep = 'scan'}><ArrowLeft class="size-4" />{i18n.t('common.back')}</Button><Button onclick={verify}>{i18n.t('common.next')}<ArrowRight class="size-4" /></Button></div></div>
|
|
192
40
|
{:else if currentStep === 'recovery'}
|
|
193
|
-
|
|
194
|
-
<Card.Card class="border-border/60">
|
|
195
|
-
<Card.CardContent class="space-y-6 p-8">
|
|
196
|
-
<div class="text-center space-y-2">
|
|
197
|
-
<div class="mx-auto inline-flex h-12 w-12 items-center justify-center rounded-xl bg-green-500/10 text-green-500">
|
|
198
|
-
<CheckCircle2 class="h-6 w-6" />
|
|
199
|
-
</div>
|
|
200
|
-
<h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorRecovery')}</h2>
|
|
201
|
-
<p class="text-sm text-muted-foreground">{i18n.t('auth.twoFactorRecoveryHint')}</p>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
<div class="rounded-xl border bg-muted/20 p-4">
|
|
205
|
-
<div class="grid grid-cols-2 gap-2 font-mono text-sm">
|
|
206
|
-
{#each recoveryCodes as code, i (i)}
|
|
207
|
-
<div class="rounded-lg bg-background px-3 py-1.5 text-center text-foreground">{code}</div>
|
|
208
|
-
{/each}
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<div class="flex gap-3">
|
|
213
|
-
<Button variant="outline" class="flex-1" onclick={copyRecoveryCodes}>
|
|
214
|
-
{#if copiedRecovery}
|
|
215
|
-
<CheckCircle2 class="h-4 w-4 mr-2" />{i18n.t('common.copied')}
|
|
216
|
-
{:else}
|
|
217
|
-
<Copy class="h-4 w-4 mr-2" />{i18n.t('common.copyAll')}
|
|
218
|
-
{/if}
|
|
219
|
-
</Button>
|
|
220
|
-
<Button class="flex-1" onclick={completeSetup}>
|
|
221
|
-
{i18n.t('account.completeSetup')} <ArrowRight class="h-4 w-4 ml-2" />
|
|
222
|
-
</Button>
|
|
223
|
-
</div>
|
|
224
|
-
</Card.CardContent>
|
|
225
|
-
</Card.Card>
|
|
226
|
-
|
|
41
|
+
<div class="space-y-4"><div><h2 class="text-base font-semibold text-foreground">{i18n.t('auth.twoFactorRecovery')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('auth.twoFactorRecoveryHint')}</p></div><div class="grid grid-cols-2 gap-2 rounded-md border border-border bg-muted p-4 font-mono text-sm">{#each recoveryCodes as code (code)}<span>{code}</span>{/each}</div><div class="flex justify-end gap-2"><Button variant="outline" onclick={copyCodes}>{#if copied}<Check class="size-4" />{:else}<Copy class="size-4" />{/if}{i18n.t('common.copy')}</Button><Button onclick={() => { enabled = true; currentStep = 'intro'; }}>{i18n.t('account.completeSetup')}</Button></div>{#if enabled}<p class="text-sm text-success">{i18n.t('security.twoFactorActive')}</p>{/if}</div>
|
|
227
42
|
{:else}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
</Button>
|
|
245
|
-
</Card.CardContent>
|
|
246
|
-
</Card.Card>
|
|
43
|
+
<div class="space-y-6 p-6 text-center">
|
|
44
|
+
<div class="mx-auto flex size-12 items-center justify-center rounded-xl bg-success/10 text-success">
|
|
45
|
+
<Shield class="size-6" />
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorVerifySuccess')}</h2>
|
|
49
|
+
<p class="mt-1 text-sm text-muted-foreground">{i18n.t('security.twoFactorActive')}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Badge variant="secondary" class="mx-auto w-fit bg-success/10 text-success">
|
|
52
|
+
<CheckCircle2 class="size-3.5" />{i18n.t('securityLog.twoFactorEnabled')}
|
|
53
|
+
</Badge>
|
|
54
|
+
<Separator />
|
|
55
|
+
<Button variant="destructive" onclick={() => { enabled = false; currentStep = 'intro'; }}>
|
|
56
|
+
{i18n.t('auth.twoFactorDisable')}
|
|
57
|
+
</Button>
|
|
58
|
+
</div>
|
|
247
59
|
{/if}
|
|
248
|
-
</
|
|
249
|
-
</
|
|
60
|
+
</Card.CardContent></Card.Card>
|
|
61
|
+
</ContentPageShell>
|
|
@@ -1,156 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import
|
|
3
|
+
import { Loader2 } from '@lucide/svelte';
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
5
|
import { Input } from '../ui/input/index.js';
|
|
6
6
|
import { Label } from '../ui/label/index.js';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
7
|
+
import * as Card from '../ui/card/index.js';
|
|
8
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
10
|
+
import DescriptionList from '../content/DescriptionList.svelte';
|
|
11
|
+
import MemberList from '../content/MemberList.svelte';
|
|
12
|
+
import type { MemberSummary } from '../content/MemberList.svelte';
|
|
11
13
|
const i18n = useTranslation();
|
|
12
|
-
|
|
13
14
|
let companyName = $state('Acme Corporation');
|
|
14
15
|
let industry = $state('Technology');
|
|
15
16
|
let website = $state('https://acme.com');
|
|
16
|
-
let
|
|
17
|
-
let foundedYear = $state('2015');
|
|
18
|
-
let description = $state('Building the future of enterprise software with cutting-edge AI and cloud solutions.');
|
|
17
|
+
let description = $state('Building the future of enterprise software with dependable AI and cloud workflows.');
|
|
19
18
|
let saving = $state(false);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ name: '
|
|
23
|
-
{ name: '
|
|
24
|
-
{ name: '
|
|
25
|
-
{ name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', twoFactor: true, joined: '2022-02-14' },
|
|
26
|
-
{ name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', twoFactor: false, joined: '2023-04-05' },
|
|
19
|
+
const members: MemberSummary[] = [
|
|
20
|
+
{ id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', status: 'success' },
|
|
21
|
+
{ id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', status: 'success' },
|
|
22
|
+
{ id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', status: 'neutral' },
|
|
23
|
+
{ id: '4', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', status: 'success' },
|
|
27
24
|
];
|
|
28
|
-
|
|
29
|
-
async function handleSave() {
|
|
30
|
-
saving = true;
|
|
31
|
-
await new Promise(r => setTimeout(r, 800));
|
|
32
|
-
saving = false;
|
|
33
|
-
}
|
|
25
|
+
async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
|
|
34
26
|
</script>
|
|
35
27
|
|
|
36
|
-
<
|
|
37
|
-
<div>
|
|
38
|
-
|
|
39
|
-
<p class="
|
|
28
|
+
<ContentPageShell pageId="account-company-profile" width="wide">
|
|
29
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t("account.companyProfile")} description={i18n.t("account.companyProfileDescription")} /><Button onclick={save} disabled={saving} size="sm">{#if saving}<Loader2 class="size-3.5 animate-spin" />{/if}{i18n.t("common.save")}</Button></div>
|
|
30
|
+
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
|
31
|
+
<Card.Card><Card.CardContent class="space-y-4 p-5"><div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="company-name">{i18n.t('profile.companyName')}</Label><Input id="company-name" bind:value={companyName} /></div><div class="space-y-2"><Label for="company-industry">{i18n.t('profile.industry')}</Label><Input id="company-industry" bind:value={industry} /></div><div class="space-y-2 sm:col-span-2"><Label for="company-website">{i18n.t('profile.website')}</Label><Input id="company-website" bind:value={website} type="url" /></div></div><div class="space-y-2"><Label for="company-description">{i18n.t('profile.companyDescription')}</Label><textarea id="company-description" bind:value={description} rows={4} class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"></textarea></div></Card.CardContent></Card.Card>
|
|
32
|
+
<Card.Card><Card.CardContent class="space-y-4 p-5"><div><p class="text-xs font-medium text-muted-foreground">{i18n.t('profile.companyName')}</p><p class="mt-1 text-lg font-semibold text-foreground">{companyName}</p><p class="text-sm text-muted-foreground">{industry}</p></div><DescriptionList columns={1} items={[{ label: i18n.t('profile.website'), value: website, href: website }, { label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('profile.founded'), value: '2015' }]} /></Card.CardContent></Card.Card>
|
|
40
33
|
</div>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<Card.Card class="border-border/60">
|
|
44
|
-
<Card.CardContent class="p-0">
|
|
45
|
-
<div class="h-32 bg-gradient-to-r from-primary/20 via-primary/10 to-accent/20 relative rounded-t-xl">
|
|
46
|
-
<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">
|
|
47
|
-
<Upload class="h-4 w-4" />
|
|
48
|
-
</button>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="relative px-6 pb-5">
|
|
51
|
-
<div class="-mt-8 flex items-end gap-4">
|
|
52
|
-
<div class="ring-4 ring-card rounded-xl overflow-hidden shrink-0">
|
|
53
|
-
<div class="flex h-16 w-16 items-center justify-center bg-primary/10 text-primary text-xl font-bold">
|
|
54
|
-
{companyName.split(' ').map(n => n[0]).join('').slice(0, 2)}
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="pb-1">
|
|
58
|
-
<h3 class="text-lg font-semibold text-foreground">{companyName}</h3>
|
|
59
|
-
<p class="text-sm text-muted-foreground">{industry}</p>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</Card.CardContent>
|
|
64
|
-
</Card.Card>
|
|
65
|
-
|
|
66
|
-
<!-- Company Details Form -->
|
|
67
|
-
<Card.Card class="border-border/60">
|
|
68
|
-
<Card.CardHeader>
|
|
69
|
-
<Card.CardTitle class="text-base">{i18n.t('profile.companyName')}</Card.CardTitle>
|
|
70
|
-
</Card.CardHeader>
|
|
71
|
-
<Card.CardContent class="space-y-4">
|
|
72
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
73
|
-
<div class="space-y-2">
|
|
74
|
-
<Label for="company-name">{i18n.t('profile.companyName')}</Label>
|
|
75
|
-
<Input id="company-name" bind:value={companyName} />
|
|
76
|
-
</div>
|
|
77
|
-
<div class="space-y-2">
|
|
78
|
-
<Label for="company-industry">{i18n.t('profile.industry')}</Label>
|
|
79
|
-
<Input id="company-industry" bind:value={industry} />
|
|
80
|
-
</div>
|
|
81
|
-
<div class="space-y-2">
|
|
82
|
-
<Label for="company-website">{i18n.t('profile.website')}</Label>
|
|
83
|
-
<Input id="company-website" bind:value={website} type="url" />
|
|
84
|
-
</div>
|
|
85
|
-
<div class="space-y-2">
|
|
86
|
-
<Label for="company-employees">{i18n.t('profile.employees')}</Label>
|
|
87
|
-
<Input id="company-employees" bind:value={employeeCount} />
|
|
88
|
-
</div>
|
|
89
|
-
<div class="space-y-2">
|
|
90
|
-
<Label for="company-founded">{i18n.t('profile.founded')}</Label>
|
|
91
|
-
<Input id="company-founded" bind:value={foundedYear} />
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="space-y-2">
|
|
95
|
-
<Label for="company-description">{i18n.t('profile.companyDescription')}</Label>
|
|
96
|
-
<textarea
|
|
97
|
-
id="company-description"
|
|
98
|
-
bind:value={description}
|
|
99
|
-
rows={3}
|
|
100
|
-
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"
|
|
101
|
-
></textarea>
|
|
102
|
-
</div>
|
|
103
|
-
<div class="flex justify-end">
|
|
104
|
-
<Button onclick={handleSave} disabled={saving}>
|
|
105
|
-
{#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
|
|
106
|
-
{i18n.t('common.save')}
|
|
107
|
-
</Button>
|
|
108
|
-
</div>
|
|
109
|
-
</Card.CardContent>
|
|
110
|
-
</Card.Card>
|
|
111
|
-
|
|
112
|
-
<!-- Members -->
|
|
113
|
-
<Card.Card class="border-border/60">
|
|
114
|
-
<Card.CardHeader>
|
|
115
|
-
<Card.CardTitle class="text-base">{i18n.t('profileSections.members')}</Card.CardTitle>
|
|
116
|
-
</Card.CardHeader>
|
|
117
|
-
<Card.CardContent class="p-0">
|
|
118
|
-
<Table.Root>
|
|
119
|
-
<Table.Header>
|
|
120
|
-
<Table.Row>
|
|
121
|
-
<Table.Head>{i18n.t('profile.name')}</Table.Head>
|
|
122
|
-
<Table.Head>{i18n.t('account.role')}</Table.Head>
|
|
123
|
-
<Table.Head>2FA</Table.Head>
|
|
124
|
-
<Table.Head class="text-right">{i18n.t('account.joined')}</Table.Head>
|
|
125
|
-
</Table.Row>
|
|
126
|
-
</Table.Header>
|
|
127
|
-
<Table.Body>
|
|
128
|
-
{#each members as member (member.email)}
|
|
129
|
-
<Table.Row>
|
|
130
|
-
<Table.Cell>
|
|
131
|
-
<div class="flex items-center gap-3">
|
|
132
|
-
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-semibold">
|
|
133
|
-
{member.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
|
|
134
|
-
</div>
|
|
135
|
-
<div class="min-w-0">
|
|
136
|
-
<p class="truncate text-sm font-medium text-foreground">{member.name}</p>
|
|
137
|
-
<p class="truncate text-xs text-muted-foreground">{member.email}</p>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</Table.Cell>
|
|
141
|
-
<Table.Cell><Badge variant="secondary" class="text-[10px]">{member.role}</Badge></Table.Cell>
|
|
142
|
-
<Table.Cell>
|
|
143
|
-
{#if member.twoFactor}
|
|
144
|
-
<ShieldCheck class="h-4 w-4 text-green-500" />
|
|
145
|
-
{:else}
|
|
146
|
-
<ShieldOff class="h-4 w-4 text-muted-foreground/50" />
|
|
147
|
-
{/if}
|
|
148
|
-
</Table.Cell>
|
|
149
|
-
<Table.Cell class="text-right text-muted-foreground">{member.joined}</Table.Cell>
|
|
150
|
-
</Table.Row>
|
|
151
|
-
{/each}
|
|
152
|
-
</Table.Body>
|
|
153
|
-
</Table.Root>
|
|
154
|
-
</Card.CardContent>
|
|
155
|
-
</Card.Card>
|
|
156
|
-
</div>
|
|
34
|
+
<section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('profileSections.members')}</h2><MemberList {members} /></section>
|
|
35
|
+
</ContentPageShell>
|