@svadmin/ui 0.34.10 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/app.css +746 -22
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/components/AdminApp.svelte +94 -15
  4. package/dist/components/AdminApp.svelte.d.ts +32 -2
  5. package/dist/components/ApiSettings.svelte +191 -0
  6. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  7. package/dist/components/AutoForm.svelte +47 -42
  8. package/dist/components/AutoTable.svelte +29 -24
  9. package/dist/components/ErrorPage.svelte +58 -0
  10. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  11. package/dist/components/FieldRenderer.svelte +2 -2
  12. package/dist/components/ForgotPasswordPage.svelte +111 -68
  13. package/dist/components/Header.svelte +6 -7
  14. package/dist/components/IntegrationsSettings.svelte +70 -0
  15. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  16. package/dist/components/Layout.svelte +7 -7
  17. package/dist/components/LoginPage.svelte +207 -105
  18. package/dist/components/LoginPage.svelte.d.ts +3 -15
  19. package/dist/components/NotificationsSettings.svelte +115 -0
  20. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  21. package/dist/components/PageHeader.svelte +9 -7
  22. package/dist/components/RegisterPage.svelte +103 -65
  23. package/dist/components/ResourceOperationsPage.svelte +151 -0
  24. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  25. package/dist/components/SecuritySettings.svelte +179 -0
  26. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  27. package/dist/components/SettingsPage.svelte +120 -87
  28. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  29. package/dist/components/ShowPage.svelte +9 -9
  30. package/dist/components/Sidebar.svelte +278 -189
  31. package/dist/components/SidebarItem.svelte +77 -18
  32. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  33. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  34. package/dist/components/UpdatePasswordPage.svelte +86 -48
  35. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  36. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  37. package/dist/components/account/GetStartedPage.svelte +123 -0
  38. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  39. package/dist/components/account/ImportMembersPage.svelte +143 -0
  40. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  41. package/dist/components/account/MembersStarterPage.svelte +104 -0
  42. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  43. package/dist/components/account/SecurityLogPage.svelte +135 -0
  44. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  45. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  46. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  47. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  48. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  49. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  50. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  51. package/dist/components/account/TeamMembersPage.svelte +98 -0
  52. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  53. package/dist/components/network/TeamCrewTablePage.svelte +155 -0
  54. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  55. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  57. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  58. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  59. package/dist/components/profile/ProfileCard.svelte +216 -0
  60. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  61. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  62. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  63. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  64. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  65. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  66. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  67. package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
  68. package/dist/index.d.ts +23 -0
  69. package/dist/index.js +27 -0
  70. package/dist/router-state.svelte.js +17 -0
  71. package/package.json +2 -2
@@ -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" data-svadmin-content-page="account">
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>
@@ -0,0 +1,3 @@
1
+ declare const CompanyProfilePage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type CompanyProfilePage = ReturnType<typeof CompanyProfilePage>;
3
+ export default CompanyProfilePage;
@@ -0,0 +1,123 @@
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 { Badge } from '../ui/badge/index.js';
6
+ import { Camera, UserPlus, Puzzle, Users, CheckCircle2, ArrowRight, Sparkles } from '@lucide/svelte';
7
+ import type { Component } from 'svelte';
8
+
9
+ interface Step {
10
+ key: string;
11
+ title: string;
12
+ description: string;
13
+ icon: Component;
14
+ completed: boolean;
15
+ }
16
+
17
+ let currentStep = $state(0);
18
+
19
+ let steps = $state<Step[]>([
20
+ { key: 'avatar', title: t('account.uploadAvatar'), description: t('account.getStartedDescription'), icon: Camera, completed: false },
21
+ { key: 'profile', title: t('account.setupProfile'), description: t('account.getStartedDescription'), icon: UserPlus, completed: false },
22
+ { key: 'connect', title: t('account.connectProvider'), description: t('account.getStartedDescription'), icon: Puzzle, completed: false },
23
+ { key: 'team', title: t('account.setupTeam'), description: t('account.getStartedDescription'), icon: Users, completed: false },
24
+ ]);
25
+
26
+ const completedCount = $derived(steps.filter(s => s.completed).length);
27
+ const progressPercent = $derived(Math.round((completedCount / steps.length) * 100));
28
+ const allCompleted = $derived(completedCount === steps.length);
29
+
30
+ function markComplete(index: number) {
31
+ steps[index] = { ...steps[index], completed: true };
32
+ if (currentStep < steps.length - 1) {
33
+ currentStep = index + 1;
34
+ }
35
+ }
36
+
37
+ function goToStep(index: number) {
38
+ if (index <= completedCount) {
39
+ currentStep = index;
40
+ }
41
+ }
42
+
43
+ function skipStep() {
44
+ if (currentStep < steps.length - 1) {
45
+ currentStep++;
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <div class="space-y-6" data-svadmin-content-page="account">
51
+ <div class="flex items-start justify-between gap-4">
52
+ <div>
53
+ <h2 class="text-xl font-semibold text-foreground">{t('account.getStarted')}</h2>
54
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.getStartedDescription')}</p>
55
+ </div>
56
+ <Badge variant="secondary">{progressPercent}%</Badge>
57
+ </div>
58
+
59
+ <!-- Progress bar -->
60
+ <div class="space-y-1.5">
61
+ <div class="h-2 rounded-full bg-muted overflow-hidden">
62
+ <div class="h-full rounded-full bg-primary transition-all duration-500" style="width: {progressPercent}%"></div>
63
+ </div>
64
+ <p class="text-xs text-muted-foreground">{t('account.step', { current: completedCount, total: steps.length })}</p>
65
+ </div>
66
+
67
+ {#if allCompleted}
68
+ <Card.Card class="border-success/30 bg-success/5">
69
+ <Card.CardContent class="flex items-center gap-4 p-6">
70
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-success/10 text-success">
71
+ <Sparkles class="h-6 w-6" />
72
+ </div>
73
+ <div>
74
+ <h3 class="font-semibold text-success">{t('account.completeSetup')}</h3>
75
+ <p class="text-sm text-success/70">{t('account.getStartedDescription')}</p>
76
+ </div>
77
+ </Card.CardContent>
78
+ </Card.Card>
79
+ {:else}
80
+ <!-- Step cards -->
81
+ <div class="space-y-3">
82
+ {#each steps as step, i (i)}
83
+ <Card.Card class="border-border/60 transition-all {currentStep === i ? 'border-primary/40 shadow-sm' : ''} {step.completed ? 'opacity-60' : ''}">
84
+ <Card.CardContent class="p-4">
85
+ <div class="flex items-start gap-4">
86
+ <button
87
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-colors
88
+ {step.completed ? 'bg-success/10 text-success' : currentStep === i ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}"
89
+ onclick={() => goToStep(i)}
90
+ >
91
+ {#if step.completed}
92
+ <CheckCircle2 class="h-5 w-5" />
93
+ {:else}
94
+ <step.icon class="h-5 w-5" />
95
+ {/if}
96
+ </button>
97
+ <div class="flex-1 min-w-0">
98
+ <div class="flex items-center gap-2">
99
+ <h4 class="font-medium text-foreground">{step.title}</h4>
100
+ {#if currentStep === i && !step.completed}
101
+ <Badge variant="outline" class="text-[10px]">{t('account.step', { current: i + 1, total: steps.length })}</Badge>
102
+ {/if}
103
+ </div>
104
+ <p class="text-sm text-muted-foreground mt-0.5">{step.description}</p>
105
+
106
+ {#if currentStep === i && !step.completed}
107
+ <div class="mt-3 flex gap-2">
108
+ <Button size="sm" onclick={() => markComplete(i)}>
109
+ {t('account.completeSetup')} <ArrowRight class="h-3.5 w-3.5 ml-1" />
110
+ </Button>
111
+ <Button size="sm" variant="ghost" onclick={skipStep}>
112
+ {t('account.skipSetup')}
113
+ </Button>
114
+ </div>
115
+ {/if}
116
+ </div>
117
+ </div>
118
+ </Card.CardContent>
119
+ </Card.Card>
120
+ {/each}
121
+ </div>
122
+ {/if}
123
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { Component } from 'svelte';
2
+ declare const GetStartedPage: Component<Record<string, never>, {}, "">;
3
+ type GetStartedPage = ReturnType<typeof GetStartedPage>;
4
+ export default GetStartedPage;
@@ -0,0 +1,143 @@
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 { Upload, Download, FileText, CheckCircle2, Loader2 } from '@lucide/svelte';
6
+
7
+ let isDragging = $state(false);
8
+ let fileName = $state('');
9
+ let importState = $state<'idle' | 'uploading' | 'processing' | 'complete' | 'error'>('idle');
10
+ let progress = $state(0);
11
+ let importCount = $state(0);
12
+
13
+ function handleDragOver(e: DragEvent) {
14
+ e.preventDefault();
15
+ isDragging = true;
16
+ }
17
+
18
+ function handleDragLeave() {
19
+ isDragging = false;
20
+ }
21
+
22
+ function handleDrop(e: DragEvent) {
23
+ e.preventDefault();
24
+ isDragging = false;
25
+ const file = e.dataTransfer?.files?.[0];
26
+ if (file) processFile(file);
27
+ }
28
+
29
+ function handleFileSelect(e: Event) {
30
+ const file = (e.target as HTMLInputElement).files?.[0];
31
+ if (file) processFile(file);
32
+ }
33
+
34
+ function processFile(file: File) {
35
+ fileName = file.name;
36
+ importState = 'uploading';
37
+ progress = 0;
38
+
39
+ // Simulate upload
40
+ const interval = setInterval(() => {
41
+ progress += 10;
42
+ if (progress >= 100) {
43
+ clearInterval(interval);
44
+ importState = 'processing';
45
+ // Simulate processing
46
+ setTimeout(() => {
47
+ importCount = Math.floor(Math.random() * 50) + 20;
48
+ importState = 'complete';
49
+ }, 1500);
50
+ }
51
+ }, 200);
52
+ }
53
+
54
+ function resetImport() {
55
+ fileName = '';
56
+ importState = 'idle';
57
+ progress = 0;
58
+ importCount = 0;
59
+ }
60
+ </script>
61
+
62
+ <div class="space-y-6" data-svadmin-content-page="account">
63
+ <div>
64
+ <h2 class="text-xl font-semibold text-foreground">{t('account.importMembers')}</h2>
65
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.importMembersDescription')}</p>
66
+ </div>
67
+
68
+ {#if importState === 'complete'}
69
+ <Card.Card class="border-success/30 bg-success/5">
70
+ <Card.CardContent class="flex flex-col items-center gap-4 p-8">
71
+ <div class="flex h-14 w-14 items-center justify-center rounded-2xl bg-success/10 text-success">
72
+ <CheckCircle2 class="h-7 w-7" />
73
+ </div>
74
+ <div class="text-center">
75
+ <h3 class="text-lg font-semibold text-success">{t('account.importComplete')}</h3>
76
+ <p class="text-sm text-success/70 mt-1">{t('account.imported', { count: importCount })}</p>
77
+ </div>
78
+ <Button variant="outline" onclick={resetImport}>{t('common.back')}</Button>
79
+ </Card.CardContent>
80
+ </Card.Card>
81
+ {:else}
82
+ <!-- Upload area -->
83
+ <Card.Card class="border-border/60">
84
+ <Card.CardContent class="p-6">
85
+ <label
86
+ for="file-input"
87
+ class="block cursor-pointer rounded-xl border-2 border-dashed p-8 text-center transition-colors
88
+ {isDragging ? 'border-primary bg-primary/5' : 'border-border/60 hover:border-primary/40'}"
89
+ ondragover={handleDragOver}
90
+ ondragleave={handleDragLeave}
91
+ ondrop={handleDrop}
92
+ >
93
+ {#if importState === 'idle'}
94
+ <Upload class="mx-auto h-10 w-10 text-muted-foreground" />
95
+ <p class="mt-3 text-sm font-medium text-foreground">{t('account.dragOrClick')}</p>
96
+ <p class="mt-1 text-xs text-muted-foreground">{t('account.supportedFormats')}</p>
97
+ <div class="mt-4">
98
+ <span class="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-sm font-medium shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground">
99
+ {t('common.upload')}
100
+ </span>
101
+ <input id="file-input" type="file" accept=".csv,.xlsx" class="sr-only" onchange={handleFileSelect} />
102
+ </div>
103
+ {:else if importState === 'uploading' || importState === 'processing'}
104
+ <div class="space-y-3">
105
+ <div class="flex items-center justify-center gap-2 text-sm">
106
+ {#if importState === 'processing'}
107
+ <Loader2 class="h-4 w-4 animate-spin" />
108
+ {:else}
109
+ <FileText class="h-4 w-4 text-muted-foreground" />
110
+ {/if}
111
+ <span class="font-medium">{fileName}</span>
112
+ </div>
113
+ <div class="h-2 w-48 mx-auto rounded-full bg-muted overflow-hidden">
114
+ <div class="h-full rounded-full bg-primary transition-all duration-300" style="width: {progress}%"></div>
115
+ </div>
116
+ <p class="text-xs text-muted-foreground">
117
+ {importState === 'uploading' ? t('account.importProgress') : t('account.startImport')}...
118
+ </p>
119
+ </div>
120
+ {/if}
121
+ </label>
122
+ </Card.CardContent>
123
+ </Card.Card>
124
+
125
+ <!-- Template download -->
126
+ <Card.Card class="border-border/60">
127
+ <Card.CardContent class="flex items-center justify-between p-4">
128
+ <div class="flex items-center gap-3">
129
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
130
+ <FileText class="h-4 w-4" />
131
+ </div>
132
+ <div>
133
+ <p class="text-sm font-medium text-foreground">{t('account.downloadTemplate')}</p>
134
+ <p class="text-xs text-muted-foreground">CSV template.csv</p>
135
+ </div>
136
+ </div>
137
+ <Button variant="outline" size="sm">
138
+ <Download class="h-3.5 w-3.5 mr-1" />{t('account.downloadTemplate')}
139
+ </Button>
140
+ </Card.CardContent>
141
+ </Card.Card>
142
+ {/if}
143
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ImportMembersPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ImportMembersPage = ReturnType<typeof ImportMembersPage>;
3
+ export default ImportMembersPage;
@@ -0,0 +1,104 @@
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 { Badge } from '../ui/badge/index.js';
6
+ import { Avatar } from '../ui/avatar/index.js';
7
+ import { Input } from '../ui/input/index.js';
8
+ import { Search, UserPlus, MoreHorizontal, Clock, CheckCircle2, XCircle } from '@lucide/svelte';
9
+
10
+ interface Member {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ role: string;
15
+ status: 'active' | 'invited' | 'inactive';
16
+ avatar?: string;
17
+ joinedAt: string;
18
+ }
19
+
20
+ let searchQuery = $state('');
21
+
22
+ let members = $state<Member[]>([
23
+ { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', status: 'active', joinedAt: '2023-01-15' },
24
+ { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', status: 'active', joinedAt: '2023-03-22' },
25
+ { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', status: 'invited', joinedAt: '2024-01-10' },
26
+ { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', status: 'active', joinedAt: '2023-06-05' },
27
+ { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', status: 'inactive', joinedAt: '2022-11-20' },
28
+ { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', status: 'active', joinedAt: '2023-02-14' },
29
+ ]);
30
+
31
+ const filteredMembers = $derived(
32
+ searchQuery
33
+ ? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()))
34
+ : members
35
+ );
36
+
37
+ const statusConfig = {
38
+ 'active': { icon: CheckCircle2, color: 'text-green-500', label: t('account.active') },
39
+ 'invited': { icon: Clock, color: 'text-amber-500', label: t('account.invited') },
40
+ 'inactive': { icon: XCircle, color: 'text-muted-foreground', label: t('account.inactive') },
41
+ };
42
+
43
+ const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
44
+ </script>
45
+
46
+ <div class="space-y-6" data-svadmin-content-page="account">
47
+ <div class="flex items-start justify-between gap-4">
48
+ <div>
49
+ <h2 class="text-xl font-semibold text-foreground">{t('account.membersStarter')}</h2>
50
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.membersStarterDescription')}</p>
51
+ </div>
52
+ <Button size="sm">
53
+ <UserPlus class="h-4 w-4 mr-1" />{t('account.inviteMember')}
54
+ </Button>
55
+ </div>
56
+
57
+ <!-- Search -->
58
+ <div class="relative">
59
+ <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
60
+ <Input
61
+ placeholder={t('common.search')}
62
+ bind:value={searchQuery}
63
+ class="pl-9"
64
+ />
65
+ </div>
66
+
67
+ <!-- Members list -->
68
+ <Card.Card class="border-border/60">
69
+ <Card.CardContent class="p-0">
70
+ <div class="divide-y">
71
+ {#each filteredMembers as member (member.id)}
72
+ {@const sc = statusConfig[member.status]}
73
+ <div class="flex items-center gap-3 p-4 hover:bg-accent/30 transition-colors">
74
+ <div class="shrink-0">
75
+ {#if member.avatar}
76
+ <Avatar class="h-9 w-9"><img src={member.avatar} alt={member.name} /></Avatar>
77
+ {:else}
78
+ <div class="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold">
79
+ {initials(member.name)}
80
+ </div>
81
+ {/if}
82
+ </div>
83
+ <div class="flex-1 min-w-0">
84
+ <div class="flex items-center gap-2">
85
+ <span class="text-sm font-medium text-foreground truncate">{member.name}</span>
86
+ <Badge variant="outline" class="text-[10px] shrink-0">{member.role}</Badge>
87
+ </div>
88
+ <p class="text-xs text-muted-foreground truncate">{member.email}</p>
89
+ </div>
90
+ <div class="shrink-0 flex items-center gap-3">
91
+ <span class="flex items-center gap-1 text-xs {sc.color}">
92
+ <sc.icon class="h-3 w-3" />
93
+ {sc.label}
94
+ </span>
95
+ <Button variant="ghost" size="icon-sm">
96
+ <MoreHorizontal class="h-4 w-4" />
97
+ </Button>
98
+ </div>
99
+ </div>
100
+ {/each}
101
+ </div>
102
+ </Card.CardContent>
103
+ </Card.Card>
104
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const MembersStarterPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type MembersStarterPage = ReturnType<typeof MembersStarterPage>;
3
+ export default MembersStarterPage;
@@ -0,0 +1,135 @@
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 { Search, Download } from '@lucide/svelte';
7
+
8
+ interface LogEntry {
9
+ id: string;
10
+ event: string;
11
+ eventLabel: string;
12
+ ip: string;
13
+ device: string;
14
+ location: string;
15
+ time: string;
16
+ severity: 'info' | 'warning' | 'critical';
17
+ }
18
+
19
+ let searchQuery = $state('');
20
+ let filterSeverity = $state<'all' | 'info' | 'warning' | 'critical'>('all');
21
+
22
+ const logs: LogEntry[] = [
23
+ { id: '1', event: 'loginSuccess', eventLabel: t('securityLog.loginSuccess'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '2 minutes ago', severity: 'info' },
24
+ { id: '2', event: 'apiKeyCreated', eventLabel: t('securityLog.apiKeyCreated'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 hour ago', severity: 'info' },
25
+ { id: '3', event: 'loginFailed', eventLabel: t('securityLog.loginFailed'), ip: '203.0.113.42', device: 'Firefox / Windows', location: 'Unknown', time: '3 hours ago', severity: 'warning' },
26
+ { id: '4', event: 'twoFactorEnabled', eventLabel: t('securityLog.twoFactorEnabled'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 day ago', severity: 'info' },
27
+ { id: '5', event: 'passwordChanged', eventLabel: t('securityLog.passwordChanged'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '2 days ago', severity: 'info' },
28
+ { id: '6', event: 'loginFailed', eventLabel: t('securityLog.loginFailed'), ip: '198.51.100.77', device: 'Safari / iOS', location: 'Beijing, CN', time: '3 days ago', severity: 'critical' },
29
+ { id: '7', event: 'sessionRevoked', eventLabel: t('securityLog.sessionRevoked'), ip: '192.168.1.105', device: 'Safari / iOS', location: 'Shanghai, CN', time: '5 days ago', severity: 'info' },
30
+ { id: '8', event: 'apiKeyRevoked', eventLabel: t('securityLog.apiKeyRevoked'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 week ago', severity: 'warning' },
31
+ ];
32
+
33
+ const filteredLogs = $derived(
34
+ logs.filter(l => {
35
+ if (filterSeverity !== 'all' && l.severity !== filterSeverity) return false;
36
+ if (searchQuery) {
37
+ const q = searchQuery.toLowerCase();
38
+ return l.eventLabel.toLowerCase().includes(q) || l.ip.includes(q) || l.device.toLowerCase().includes(q);
39
+ }
40
+ return true;
41
+ })
42
+ );
43
+
44
+ </script>
45
+
46
+ <div class="space-y-6" data-svadmin-content-page="account">
47
+ <div class="flex items-start justify-between gap-4">
48
+ <div>
49
+ <h2 class="text-xl font-semibold text-foreground">{t('account.securityLog')}</h2>
50
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.securityLogDescription')}</p>
51
+ </div>
52
+ <Button variant="outline" size="sm">
53
+ <Download class="h-3.5 w-3.5 mr-1" />{t('common.export')}
54
+ </Button>
55
+ </div>
56
+
57
+ <!-- Filters -->
58
+ <div class="flex flex-wrap gap-3">
59
+ <div class="relative flex-1 min-w-[200px]">
60
+ <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
61
+ <Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
62
+ </div>
63
+ <div class="flex rounded-lg border border-input overflow-hidden">
64
+ {#each ['all', 'info', 'warning', 'critical'] as sev (sev)}
65
+ <button
66
+ class="px-3 py-1.5 text-xs font-medium transition-colors capitalize
67
+ {filterSeverity === sev ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
68
+ onclick={() => filterSeverity = sev as typeof filterSeverity}
69
+ >{sev === 'all' ? t('common.filter') : sev}</button>
70
+ {/each}
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Log table -->
75
+ <Card.Card class="border-border/60">
76
+ <Card.CardContent class="p-0">
77
+ <div class="hidden overflow-x-auto md:block">
78
+ <table class="w-full">
79
+ <thead>
80
+ <tr class="border-b bg-muted/30">
81
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('securityLog.event')}</th>
82
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('securityLog.ip')}</th>
83
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('securityLog.device')}</th>
84
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('securityLog.location')}</th>
85
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('securityLog.time')}</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody class="divide-y">
89
+ {#each filteredLogs as log (log.id)}
90
+ <tr class="hover:bg-accent/30 transition-colors">
91
+ <td class="px-4 py-3">
92
+ <div class="flex items-center gap-2">
93
+ <span class="inline-flex h-2 w-2 rounded-full {log.severity === 'critical' ? 'bg-red-500' : log.severity === 'warning' ? 'bg-amber-500' : 'bg-blue-500'}"></span>
94
+ <span class="text-sm text-foreground">{log.eventLabel}</span>
95
+ </div>
96
+ </td>
97
+ <td class="px-4 py-3 text-sm text-muted-foreground font-mono">{log.ip}</td>
98
+ <td class="px-4 py-3 text-sm text-muted-foreground">{log.device}</td>
99
+ <td class="px-4 py-3 text-sm text-muted-foreground">{log.location}</td>
100
+ <td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{log.time}</td>
101
+ </tr>
102
+ {/each}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ <div class="divide-y md:hidden">
107
+ {#each filteredLogs as log (log.id)}
108
+ <div class="space-y-3 p-4">
109
+ <div class="flex items-start justify-between gap-3">
110
+ <div class="flex min-w-0 items-center gap-2">
111
+ <span class="inline-flex h-2 w-2 shrink-0 rounded-full {log.severity === 'critical' ? 'bg-red-500' : log.severity === 'warning' ? 'bg-amber-500' : 'bg-blue-500'}"></span>
112
+ <span class="truncate text-sm font-medium text-foreground">{log.eventLabel}</span>
113
+ </div>
114
+ <span class="shrink-0 text-xs text-muted-foreground">{log.time}</span>
115
+ </div>
116
+ <div class="grid gap-2 text-xs text-muted-foreground">
117
+ <div class="flex items-center justify-between gap-3">
118
+ <span>{t('securityLog.ip')}</span>
119
+ <span class="font-mono text-foreground">{log.ip}</span>
120
+ </div>
121
+ <div class="flex items-center justify-between gap-3">
122
+ <span>{t('securityLog.device')}</span>
123
+ <span class="truncate text-foreground">{log.device}</span>
124
+ </div>
125
+ <div class="flex items-center justify-between gap-3">
126
+ <span>{t('securityLog.location')}</span>
127
+ <span class="truncate text-foreground">{log.location}</span>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ {/each}
132
+ </div>
133
+ </Card.CardContent>
134
+ </Card.Card>
135
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SecurityLogPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SecurityLogPage = ReturnType<typeof SecurityLogPage>;
3
+ export default SecurityLogPage;