@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,131 @@
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 { Switch } from '../ui/switch/index.js';
8
+ import { Badge } from '../ui/badge/index.js';
9
+ import { Loader2, Shield, Building2, Lock, Globe } from '@lucide/svelte';
10
+
11
+ let ssoEnabled = $state(false);
12
+ let ssoProvider = $state('');
13
+ let domainRestriction = $state('');
14
+ let passwordPolicy = $state('strong');
15
+ let sessionTimeout = $state('30');
16
+ let ipWhitelist = $state('');
17
+ let auditLogging = $state(true);
18
+ let dataRetention = $state('365');
19
+ let saving = $state(false);
20
+
21
+ async function handleSave() {
22
+ saving = true;
23
+ await new Promise(r => setTimeout(r, 800));
24
+ saving = false;
25
+ }
26
+ </script>
27
+
28
+ <div class="space-y-6" data-svadmin-content-page="account">
29
+ <div class="flex items-start justify-between gap-4">
30
+ <div>
31
+ <h2 class="text-xl font-semibold text-foreground">{t('account.settingsEnterprise')}</h2>
32
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.settingsEnterpriseDescription')}</p>
33
+ </div>
34
+ <Badge variant="secondary"><Building2 class="h-3 w-3 mr-1" />Enterprise</Badge>
35
+ </div>
36
+
37
+ <!-- SSO -->
38
+ <Card.Card class="border-border/60">
39
+ <Card.CardHeader>
40
+ <Card.CardTitle class="flex items-center gap-2 text-base">
41
+ <Shield class="h-4 w-4 text-muted-foreground" />
42
+ 单点登录 (SSO)
43
+ </Card.CardTitle>
44
+ <Card.CardDescription>配置企业身份提供商以实现统一登录体验。</Card.CardDescription>
45
+ </Card.CardHeader>
46
+ <Card.CardContent class="space-y-4">
47
+ <div class="flex items-center justify-between gap-4">
48
+ <div class="space-y-1">
49
+ <Label for="sso-enabled">启用 SSO</Label>
50
+ <p class="text-xs text-muted-foreground">允许员工通过企业 IdP 登录</p>
51
+ </div>
52
+ <Switch id="sso-enabled" bind:checked={ssoEnabled} />
53
+ </div>
54
+ {#if ssoEnabled}
55
+ <div class="grid gap-4 sm:grid-cols-2">
56
+ <div class="space-y-2">
57
+ <Label for="sso-provider">SSO 提供商</Label>
58
+ <Input id="sso-provider" bind:value={ssoProvider} placeholder="Okta, Azure AD, etc." />
59
+ </div>
60
+ <div class="space-y-2">
61
+ <Label for="domain-restriction">域名限制</Label>
62
+ <Input id="domain-restriction" bind:value={domainRestriction} placeholder="@company.com" />
63
+ </div>
64
+ </div>
65
+ {/if}
66
+ </Card.CardContent>
67
+ </Card.Card>
68
+
69
+ <!-- Security Policy -->
70
+ <Card.Card class="border-border/60">
71
+ <Card.CardHeader>
72
+ <Card.CardTitle class="flex items-center gap-2 text-base">
73
+ <Lock class="h-4 w-4 text-muted-foreground" />
74
+ 安全策略
75
+ </Card.CardTitle>
76
+ </Card.CardHeader>
77
+ <Card.CardContent class="space-y-4">
78
+ <div class="grid gap-4 sm:grid-cols-2">
79
+ <div class="space-y-2">
80
+ <Label for="password-policy">密码策略</Label>
81
+ <select id="password-policy" bind:value={passwordPolicy} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring">
82
+ <option value="basic">基本</option>
83
+ <option value="strong">强密码</option>
84
+ <option value="very-strong">非常强</option>
85
+ </select>
86
+ </div>
87
+ <div class="space-y-2">
88
+ <Label for="session-timeout">会话超时(分钟)</Label>
89
+ <Input id="session-timeout" bind:value={sessionTimeout} type="number" />
90
+ </div>
91
+ </div>
92
+
93
+ <div class="space-y-2">
94
+ <Label for="ip-whitelist">IP 白名单</Label>
95
+ <Input id="ip-whitelist" bind:value={ipWhitelist} placeholder="10.0.0.0/8, 192.168.1.0/24" />
96
+ <p class="text-xs text-muted-foreground">留空表示不限制 IP</p>
97
+ </div>
98
+ </Card.CardContent>
99
+ </Card.Card>
100
+
101
+ <!-- Data & Compliance -->
102
+ <Card.Card class="border-border/60">
103
+ <Card.CardHeader>
104
+ <Card.CardTitle class="flex items-center gap-2 text-base">
105
+ <Globe class="h-4 w-4 text-muted-foreground" />
106
+ 数据与合规
107
+ </Card.CardTitle>
108
+ </Card.CardHeader>
109
+ <Card.CardContent class="space-y-4">
110
+ <div class="flex items-center justify-between gap-4">
111
+ <div class="space-y-1">
112
+ <Label for="audit-logging">审计日志</Label>
113
+ <p class="text-xs text-muted-foreground">记录所有管理操作以备合规审查</p>
114
+ </div>
115
+ <Switch id="audit-logging" bind:checked={auditLogging} />
116
+ </div>
117
+
118
+ <div class="space-y-2">
119
+ <Label for="data-retention">数据保留天数</Label>
120
+ <Input id="data-retention" bind:value={dataRetention} type="number" />
121
+ </div>
122
+
123
+ <div class="flex justify-end">
124
+ <Button onclick={handleSave} disabled={saving}>
125
+ {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
126
+ {t('common.save')}
127
+ </Button>
128
+ </div>
129
+ </Card.CardContent>
130
+ </Card.Card>
131
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SettingsEnterprisePage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SettingsEnterprisePage = ReturnType<typeof SettingsEnterprisePage>;
3
+ export default SettingsEnterprisePage;
@@ -0,0 +1,81 @@
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 { Switch } from '../ui/switch/index.js';
8
+ import { Separator } from '../ui/separator/index.js';
9
+ import { Loader2 } from '@lucide/svelte';
10
+
11
+ let displayName = $state('Alex Chen');
12
+ let email = $state('alex@example.com');
13
+ let emailNotifications = $state(true);
14
+ let pushNotifications = $state(true);
15
+ let twoFactor = $state(false);
16
+ let saving = $state(false);
17
+
18
+ async function handleSave() {
19
+ saving = true;
20
+ await new Promise(r => setTimeout(r, 800));
21
+ saving = false;
22
+ }
23
+ </script>
24
+
25
+ <div class="space-y-6" data-svadmin-content-page="account">
26
+ <div>
27
+ <h2 class="text-xl font-semibold text-foreground">{t('account.settingsPlain')}</h2>
28
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.settingsPlainDescription')}</p>
29
+ </div>
30
+
31
+ <Card.Card class="border-border/60">
32
+ <Card.CardHeader>
33
+ <Card.CardTitle class="text-base">{t('settings.general')}</Card.CardTitle>
34
+ </Card.CardHeader>
35
+ <Card.CardContent class="space-y-4">
36
+ <div class="grid gap-4 sm:grid-cols-2">
37
+ <div class="space-y-2">
38
+ <Label for="display-name">{t('profile.name')}</Label>
39
+ <Input id="display-name" bind:value={displayName} />
40
+ </div>
41
+ <div class="space-y-2">
42
+ <Label for="email">Email</Label>
43
+ <Input id="email" bind:value={email} type="email" />
44
+ </div>
45
+ </div>
46
+
47
+ <Separator />
48
+
49
+ <div class="flex items-center justify-between gap-4">
50
+ <div class="space-y-1">
51
+ <Label for="email-notif">{t('notifications.emailTitle')}</Label>
52
+ <p class="text-xs text-muted-foreground">{t('notifications.emailDescription')}</p>
53
+ </div>
54
+ <Switch id="email-notif" bind:checked={emailNotifications} />
55
+ </div>
56
+
57
+ <div class="flex items-center justify-between gap-4">
58
+ <div class="space-y-1">
59
+ <Label for="push-notif">{t('notifications.pushTitle')}</Label>
60
+ <p class="text-xs text-muted-foreground">{t('notifications.pushDescription')}</p>
61
+ </div>
62
+ <Switch id="push-notif" bind:checked={pushNotifications} />
63
+ </div>
64
+
65
+ <div class="flex items-center justify-between gap-4">
66
+ <div class="space-y-1">
67
+ <Label for="two-factor">{t('security.twoFactorAuth')}</Label>
68
+ <p class="text-xs text-muted-foreground">{t('security.twoFactorDescription')}</p>
69
+ </div>
70
+ <Switch id="two-factor" bind:checked={twoFactor} />
71
+ </div>
72
+
73
+ <div class="flex justify-end">
74
+ <Button onclick={handleSave} disabled={saving}>
75
+ {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
76
+ {t('common.save')}
77
+ </Button>
78
+ </div>
79
+ </Card.CardContent>
80
+ </Card.Card>
81
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SettingsPlainPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SettingsPlainPage = ReturnType<typeof SettingsPlainPage>;
3
+ export default SettingsPlainPage;
@@ -0,0 +1,98 @@
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 { Label } from '../ui/label/index.js';
6
+ import { Switch } from '../ui/switch/index.js';
7
+ import { Separator } from '../ui/separator/index.js';
8
+ import { Loader2, PanelLeft } from '@lucide/svelte';
9
+
10
+ let defaultCollapsed = $state(false);
11
+ let pinSidebar = $state(true);
12
+ let showLabels = $state(true);
13
+ let showIcons = $state(true);
14
+ let density = $state<'compact' | 'standard'>('standard');
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.settingsSidebar')}</h2>
27
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.settingsSidebarDescription')}</p>
28
+ </div>
29
+
30
+ <Card.Card class="border-border/60">
31
+ <Card.CardHeader>
32
+ <Card.CardTitle class="flex items-center gap-2 text-base">
33
+ <PanelLeft class="h-4 w-4 text-muted-foreground" />
34
+ {t('account.settingsSidebar')}
35
+ </Card.CardTitle>
36
+ </Card.CardHeader>
37
+ <Card.CardContent class="space-y-4">
38
+ <div class="flex items-center justify-between gap-4">
39
+ <div class="space-y-1">
40
+ <Label for="default-collapsed">默认折叠</Label>
41
+ <p class="text-xs text-muted-foreground">启动时自动折叠侧边栏</p>
42
+ </div>
43
+ <Switch id="default-collapsed" bind:checked={defaultCollapsed} />
44
+ </div>
45
+
46
+ <Separator />
47
+
48
+ <div class="flex items-center justify-between gap-4">
49
+ <div class="space-y-1">
50
+ <Label for="pin-sidebar">{t('settings.general')}</Label>
51
+ <p class="text-xs text-muted-foreground">固定侧边栏,防止自动收起</p>
52
+ </div>
53
+ <Switch id="pin-sidebar" bind:checked={pinSidebar} />
54
+ </div>
55
+
56
+ <div class="flex items-center justify-between gap-4">
57
+ <div class="space-y-1">
58
+ <Label for="show-labels">显示标签文字</Label>
59
+ <p class="text-xs text-muted-foreground">在图标旁显示菜单项名称</p>
60
+ </div>
61
+ <Switch id="show-labels" bind:checked={showLabels} />
62
+ </div>
63
+
64
+ <div class="flex items-center justify-between gap-4">
65
+ <div class="space-y-1">
66
+ <Label for="show-icons">显示图标</Label>
67
+ <p class="text-xs text-muted-foreground">在标签旁显示菜单图标</p>
68
+ </div>
69
+ <Switch id="show-icons" bind:checked={showIcons} />
70
+ </div>
71
+
72
+ <Separator />
73
+
74
+ <div class="space-y-2">
75
+ <Label>{t('settings.sidebarDensity')}</Label>
76
+ <div class="flex rounded-lg border border-input overflow-hidden">
77
+ <button
78
+ class="flex-1 px-4 py-2 text-sm font-medium transition-colors
79
+ {density === 'compact' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
80
+ onclick={() => density = 'compact'}
81
+ >{t('settings.compact')}</button>
82
+ <button
83
+ class="flex-1 px-4 py-2 text-sm font-medium transition-colors
84
+ {density === 'standard' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
85
+ onclick={() => density = 'standard'}
86
+ >{t('settings.standard')}</button>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="flex justify-end">
91
+ <Button onclick={handleSave} disabled={saving}>
92
+ {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
93
+ {t('common.save')}
94
+ </Button>
95
+ </div>
96
+ </Card.CardContent>
97
+ </Card.Card>
98
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SettingsSidebarPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SettingsSidebarPage = ReturnType<typeof SettingsSidebarPage>;
3
+ export default SettingsSidebarPage;
@@ -0,0 +1,98 @@
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, UserPlus, MoreHorizontal } from '@lucide/svelte';
7
+
8
+ interface TeamMember {
9
+ id: string;
10
+ name: string;
11
+ email: string;
12
+ role: 'Admin' | 'Editor' | 'Viewer';
13
+ avatar?: string;
14
+ department: string;
15
+ status: 'active' | 'invited' | 'inactive';
16
+ lastActive: string;
17
+ }
18
+
19
+ let searchQuery = $state('');
20
+
21
+ let members = $state<TeamMember[]>([
22
+ { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: 'Just now' },
23
+ { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', department: 'Design', status: 'active', lastActive: '5 min ago' },
24
+ { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', department: 'Marketing', status: 'invited', lastActive: 'Pending' },
25
+ { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', department: 'Engineering', status: 'active', lastActive: '1 hour ago' },
26
+ { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', department: 'Sales', status: 'inactive', lastActive: '2 weeks ago' },
27
+ { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: '10 min ago' },
28
+ ]);
29
+
30
+ const filtered = $derived(
31
+ searchQuery
32
+ ? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
33
+ : members
34
+ );
35
+
36
+ const roleColors = {
37
+ 'Admin': 'bg-red-500/10 text-red-600',
38
+ 'Editor': 'bg-blue-500/10 text-blue-600',
39
+ 'Viewer': 'bg-green-500/10 text-green-600',
40
+ };
41
+
42
+ const statusColors = {
43
+ 'active': 'bg-green-500/10 text-green-600',
44
+ 'invited': 'bg-amber-500/10 text-amber-600',
45
+ 'inactive': 'bg-muted text-muted-foreground',
46
+ };
47
+
48
+ const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
49
+ </script>
50
+
51
+ <div class="space-y-6" data-svadmin-content-page="account">
52
+ <div class="flex items-start justify-between gap-4">
53
+ <div>
54
+ <h2 class="text-xl font-semibold text-foreground">{t('account.membersStarter')}</h2>
55
+ <p class="mt-1 text-sm text-muted-foreground">{t('account.membersStarterDescription')}</p>
56
+ </div>
57
+ <Button size="sm">
58
+ <UserPlus class="h-4 w-4 mr-1" />{t('account.inviteMember')}
59
+ </Button>
60
+ </div>
61
+
62
+ <div class="relative">
63
+ <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
64
+ <Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
65
+ </div>
66
+
67
+ <!-- Members grid -->
68
+ <div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
69
+ {#each filtered as member (member.id)}
70
+ <Card.Card class="border-border/60 hover:border-primary/30 transition-colors">
71
+ <Card.CardContent class="p-4">
72
+ <div class="flex items-start gap-3">
73
+ <div class="shrink-0">
74
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold">
75
+ {initials(member.name)}
76
+ </div>
77
+ </div>
78
+ <div class="flex-1 min-w-0">
79
+ <div class="flex items-center gap-2">
80
+ <h4 class="text-sm font-semibold text-foreground truncate">{member.name}</h4>
81
+ </div>
82
+ <p class="text-xs text-muted-foreground truncate">{member.email}</p>
83
+ <div class="mt-2 flex flex-wrap gap-1.5">
84
+ <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {roleColors[member.role]}">{member.role}</span>
85
+ <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {statusColors[member.status]}">{member.status}</span>
86
+ </div>
87
+ <div class="mt-2 flex items-center justify-between">
88
+ <span class="text-xs text-muted-foreground">{member.department}</span>
89
+ <span class="text-xs text-muted-foreground">{member.lastActive}</span>
90
+ </div>
91
+ </div>
92
+ <Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
93
+ </div>
94
+ </Card.CardContent>
95
+ </Card.Card>
96
+ {/each}
97
+ </div>
98
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const TeamMembersPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TeamMembersPage = ReturnType<typeof TeamMembersPage>;
3
+ export default TeamMembersPage;
@@ -0,0 +1,155 @@
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, UserPlus, MoreHorizontal, CheckCircle2, Clock, XCircle } from '@lucide/svelte';
7
+
8
+ interface CrewMember {
9
+ id: string;
10
+ name: string;
11
+ email: string;
12
+ role: string;
13
+ department: string;
14
+ status: 'active' | 'away' | 'offline';
15
+ avatar?: string;
16
+ joinedAt: string;
17
+ projects: number;
18
+ }
19
+
20
+ let searchQuery = $state('');
21
+
22
+ const crew: CrewMember[] = [
23
+ { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Tech Lead', department: 'Engineering', status: 'active', joinedAt: '2022-03-15', projects: 12 },
24
+ { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Senior Designer', department: 'Design', status: 'active', joinedAt: '2022-06-22', projects: 8 },
25
+ { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Product Manager', department: 'Product', status: 'away', joinedAt: '2023-01-10', projects: 6 },
26
+ { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Backend Engineer', department: 'Engineering', status: 'active', joinedAt: '2023-04-05', projects: 10 },
27
+ { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'QA Engineer', department: 'Engineering', status: 'offline', joinedAt: '2022-11-20', projects: 4 },
28
+ { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', joinedAt: '2022-02-14', projects: 15 },
29
+ { id: '7', name: 'James Wilson', email: 'james@acme.com', role: 'DevOps Engineer', department: 'Infrastructure', status: 'active', joinedAt: '2023-08-01', projects: 7 },
30
+ { id: '8', name: 'Yuki Tanaka', email: 'yuki@acme.com', role: 'Frontend Engineer', department: 'Engineering', status: 'away', joinedAt: '2023-05-12', projects: 9 },
31
+ ];
32
+
33
+ const filtered = $derived(
34
+ searchQuery
35
+ ? crew.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
36
+ : crew
37
+ );
38
+
39
+ const statusConfig = {
40
+ 'active': { icon: CheckCircle2, color: 'text-green-500', dot: 'bg-green-500', label: t('account.active') },
41
+ 'away': { icon: Clock, color: 'text-amber-500', dot: 'bg-amber-500', label: t('account.invited') },
42
+ 'offline': { icon: XCircle, color: 'text-muted-foreground', dot: 'bg-muted-foreground', label: t('account.inactive') },
43
+ };
44
+
45
+ const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
46
+ </script>
47
+
48
+ <div class="space-y-6" data-svadmin-content-page="network">
49
+ <div class="flex items-start justify-between gap-4">
50
+ <div>
51
+ <h2 class="text-xl font-semibold text-foreground">{t('network.teamCrew')}</h2>
52
+ <p class="mt-1 text-sm text-muted-foreground">{t('network.teamCrewDescription')}</p>
53
+ </div>
54
+ <div class="flex gap-2">
55
+ <Button variant="outline" size="sm">
56
+ <Download class="h-3.5 w-3.5 mr-1" />{t('common.export')}
57
+ </Button>
58
+ <Button size="sm">
59
+ <UserPlus class="h-3.5 w-3.5 mr-1" />{t('account.inviteMember')}
60
+ </Button>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="relative">
65
+ <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
66
+ <Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
67
+ </div>
68
+
69
+ <!-- Table -->
70
+ <Card.Card class="border-border/60">
71
+ <Card.CardContent class="p-0">
72
+ <div class="hidden overflow-x-auto lg:block">
73
+ <table class="w-full">
74
+ <thead>
75
+ <tr class="border-b bg-muted/30">
76
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.member')}</th>
77
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.role')}</th>
78
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">Department</th>
79
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.status')}</th>
80
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('publicProfile.projects')}</th>
81
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.joined')}</th>
82
+ <th class="px-4 py-3 w-12"></th>
83
+ </tr>
84
+ </thead>
85
+ <tbody class="divide-y">
86
+ {#each filtered as member (member.id)}
87
+ {@const sc = statusConfig[member.status]}
88
+ <tr class="hover:bg-accent/30 transition-colors">
89
+ <td class="px-4 py-3">
90
+ <div class="flex items-center gap-3">
91
+ <div class="relative shrink-0">
92
+ <div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-semibold">
93
+ {initials(member.name)}
94
+ </div>
95
+ <span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full {sc.dot} ring-2 ring-card"></span>
96
+ </div>
97
+ <div class="min-w-0">
98
+ <p class="text-sm font-medium text-foreground truncate">{member.name}</p>
99
+ <p class="text-xs text-muted-foreground truncate">{member.email}</p>
100
+ </div>
101
+ </div>
102
+ </td>
103
+ <td class="px-4 py-3 text-sm text-foreground">{member.role}</td>
104
+ <td class="px-4 py-3 text-sm text-muted-foreground">{member.department}</td>
105
+ <td class="px-4 py-3">
106
+ <span class="flex items-center gap-1 text-xs {sc.color}">
107
+ <sc.icon class="h-3 w-3" />{sc.label}
108
+ </span>
109
+ </td>
110
+ <td class="px-4 py-3 text-sm text-foreground">{member.projects}</td>
111
+ <td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{member.joinedAt}</td>
112
+ <td class="px-4 py-3">
113
+ <Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
114
+ </td>
115
+ </tr>
116
+ {/each}
117
+ </tbody>
118
+ </table>
119
+ </div>
120
+ <div class="divide-y lg:hidden">
121
+ {#each filtered as member (member.id)}
122
+ {@const sc = statusConfig[member.status]}
123
+ <div class="space-y-3 p-4">
124
+ <div class="flex items-start justify-between gap-3">
125
+ <div class="flex min-w-0 items-center gap-3">
126
+ <div class="relative shrink-0">
127
+ <div class="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
128
+ {initials(member.name)}
129
+ </div>
130
+ <span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full {sc.dot} ring-2 ring-card"></span>
131
+ </div>
132
+ <div class="min-w-0">
133
+ <p class="truncate text-sm font-medium text-foreground">{member.name}</p>
134
+ <p class="truncate text-xs text-muted-foreground">{member.email}</p>
135
+ </div>
136
+ </div>
137
+ <Button variant="ghost" size="icon-sm" class="shrink-0"><MoreHorizontal class="h-4 w-4" /></Button>
138
+ </div>
139
+ <div class="grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
140
+ <div class="min-w-0">
141
+ <span class="block font-medium text-foreground">{member.role}</span>
142
+ <span class="truncate">{member.department}</span>
143
+ </div>
144
+ <div class="flex items-center gap-1 {sc.color}">
145
+ <sc.icon class="h-3 w-3" />{sc.label}
146
+ </div>
147
+ <div>{t('publicProfile.projects')}: <span class="font-medium text-foreground">{member.projects}</span></div>
148
+ <div>{t('account.joined')}: <span class="font-medium text-foreground">{member.joinedAt}</span></div>
149
+ </div>
150
+ </div>
151
+ {/each}
152
+ </div>
153
+ </Card.CardContent>
154
+ </Card.Card>
155
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const TeamCrewTablePage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TeamCrewTablePage = ReturnType<typeof TeamCrewTablePage>;
3
+ export default TeamCrewTablePage;