@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.
Files changed (76) hide show
  1. package/dist/app.css +462 -825
  2. package/dist/app.theme.css +462 -825
  3. package/dist/components/ErrorPage.svelte +4 -56
  4. package/dist/components/TwoFactorAuthPage.svelte +44 -232
  5. package/dist/components/account/CompanyProfilePage.svelte +21 -142
  6. package/dist/components/account/GetStartedPage.svelte +33 -115
  7. package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
  8. package/dist/components/account/SecurityLogPage.svelte +16 -131
  9. package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
  10. package/dist/components/account/SettingsPlainPage.svelte +10 -175
  11. package/dist/components/account/SettingsSidebarPage.svelte +11 -304
  12. package/dist/components/account/TeamMembersPage.svelte +18 -187
  13. package/dist/components/account/UserProfilePage.svelte +19 -196
  14. package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
  15. package/dist/components/content/ApiKeyList.svelte +13 -0
  16. package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
  17. package/dist/components/content/ContentPageHeader.svelte +31 -0
  18. package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
  19. package/dist/components/content/ContentPageShell.svelte +39 -0
  20. package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
  21. package/dist/components/content/DataState.svelte +44 -0
  22. package/dist/components/content/DataState.svelte.d.ts +13 -0
  23. package/dist/components/content/DescriptionList.svelte +27 -0
  24. package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
  25. package/dist/components/content/FileList.svelte +13 -0
  26. package/dist/components/content/FileList.svelte.d.ts +16 -0
  27. package/dist/components/content/FilterToolbar.svelte +24 -0
  28. package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
  29. package/dist/components/content/ImportDropzone.svelte +19 -0
  30. package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
  31. package/dist/components/content/IntegrationCard.svelte +16 -0
  32. package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
  33. package/dist/components/content/MemberList.svelte +16 -0
  34. package/dist/components/content/MemberList.svelte.d.ts +17 -0
  35. package/dist/components/content/MetricBlock.svelte +27 -0
  36. package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
  37. package/dist/components/content/NetworkTable.svelte +18 -0
  38. package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
  39. package/dist/components/content/NetworkTable.types.d.ts +4 -0
  40. package/dist/components/content/NetworkTable.types.js +1 -0
  41. package/dist/components/content/NetworkUserCard.svelte +17 -0
  42. package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
  43. package/dist/components/content/OtpInput.svelte +22 -0
  44. package/dist/components/content/OtpInput.svelte.d.ts +10 -0
  45. package/dist/components/content/PageToolbar.svelte +18 -0
  46. package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
  47. package/dist/components/content/ProjectCard.svelte +39 -0
  48. package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
  49. package/dist/components/content/SectionHeader.svelte +18 -0
  50. package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
  51. package/dist/components/content/SecurityEventTable.svelte +18 -0
  52. package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
  53. package/dist/components/content/StatusBadge.svelte +19 -0
  54. package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
  55. package/dist/components/content/SystemErrorState.svelte +18 -0
  56. package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
  57. package/dist/components/content/TeamCard.svelte +26 -0
  58. package/dist/components/content/TeamCard.svelte.d.ts +23 -0
  59. package/dist/components/content/TwoFactorStepper.svelte +11 -0
  60. package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
  61. package/dist/components/content/index.d.ts +32 -0
  62. package/dist/components/content/index.js +22 -0
  63. package/dist/components/network/TeamCrewTablePage.svelte +29 -181
  64. package/dist/components/network/UserCardsNFTPage.svelte +24 -137
  65. package/dist/components/profile/ActivityTimeline.svelte +17 -91
  66. package/dist/components/profile/ProfileCard.svelte +4 -4
  67. package/dist/components/profile/ProjectsGrid.svelte +28 -106
  68. package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
  69. package/dist/components/profile/PublicProfilePage.svelte +31 -181
  70. package/dist/components/profile/TeamsShowcase.svelte +12 -114
  71. package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
  72. package/dist/index.d.ts +36 -0
  73. package/dist/index.js +24 -0
  74. package/dist/reference-data.d.ts +171 -0
  75. package/dist/reference-data.js +39 -0
  76. package/package.json +1 -1
@@ -1,60 +1,8 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext } from '@svadmin/core';
3
- import { useTranslation } from '@svadmin/core/i18n';
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
- <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="error">
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 * as Card from './ui/card/index.js';
3
+ import { ArrowLeft, ArrowRight, Check, CheckCircle2, Copy, Shield } from '@lucide/svelte';
4
4
  import { Button } from './ui/button/index.js';
5
- import { Input } from './ui/input/index.js';
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 is2faEnabled = $state(false);
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
- const verificationCode = $derived(codeDigits.join(''));
22
- const qrCells = [
23
- '11111110',
24
- '10010010',
25
- '10111010',
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
- <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="auth-2fa">
85
- <div class="w-full max-w-lg space-y-6">
86
- {#if currentStep === 'intro' && !is2faEnabled}
87
- <!-- Intro -->
88
- <Card.Card class="border-border/60">
89
- <Card.CardContent class="space-y-6 p-8 text-center">
90
- <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500">
91
- <Shield class="h-8 w-8" />
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
- <!-- QR Code -->
128
- <Card.Card class="border-border/60">
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
- <!-- Verify -->
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
- <!-- Recovery codes -->
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
- <!-- 2FA enabled state -->
229
- <Card.Card class="border-border/60">
230
- <Card.CardContent class="space-y-6 p-8 text-center">
231
- <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-green-500/10 text-green-500">
232
- <Shield class="h-8 w-8" />
233
- </div>
234
- <div class="space-y-2">
235
- <h2 class="text-2xl font-semibold text-foreground">{i18n.t('auth.twoFactorVerifySuccess')}</h2>
236
- <p class="text-sm text-muted-foreground">{i18n.t('security.twoFactorActive')}</p>
237
- </div>
238
- <Badge variant="secondary" class="bg-green-500/10 text-green-600">
239
- <CheckCircle2 class="h-3 w-3 mr-1" />2FA Active
240
- </Badge>
241
- <Separator />
242
- <Button variant="destructive" onclick={disable2fa}>
243
- {i18n.t('auth.twoFactorDisable')}
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
- </div>
249
- </div>
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 * as Card from '../ui/card/index.js';
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 { Badge } from '../ui/badge/index.js';
8
- import * as Table from '../ui/table/index.js';
9
- import { Upload, Loader2, ShieldCheck, ShieldOff } from '@lucide/svelte';
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 employeeCount = $state('1,250');
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
- const members = [
22
- { name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', twoFactor: true, joined: '2022-03-15' },
23
- { name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', twoFactor: true, joined: '2022-06-22' },
24
- { name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', twoFactor: false, joined: '2023-01-10' },
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
- <div class="space-y-6" data-svadmin-content-page="account">
37
- <div>
38
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.companyProfile')}</h2>
39
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.companyProfileDescription')}</p>
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
- <!-- Company Avatar & Banner -->
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>