@svadmin/ui 0.38.7 → 0.40.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 (61) hide show
  1. package/dist/app.css +0 -4
  2. package/dist/app.theme.css +0 -4
  3. package/dist/assets/nft/nft-cover-01.jpg +0 -0
  4. package/dist/assets/nft/nft-cover-02.jpg +0 -0
  5. package/dist/assets/nft/nft-cover-03.jpg +0 -0
  6. package/dist/assets/nft/nft-cover-04.jpg +0 -0
  7. package/dist/assets/nft/nft-cover-05.jpg +0 -0
  8. package/dist/assets/nft/nft-cover-06.jpg +0 -0
  9. package/dist/assets/nft/nft-cover-07.jpg +0 -0
  10. package/dist/assets/nft/nft-cover-08.jpg +0 -0
  11. package/dist/assets/nft/nft-cover-09.jpg +0 -0
  12. package/dist/components/AdminApp.svelte +29 -12
  13. package/dist/components/ApiSettings.svelte +86 -1
  14. package/dist/components/AutoTable.svelte +40 -13
  15. package/dist/components/Breadcrumbs.svelte +39 -6
  16. package/dist/components/Breadcrumbs.svelte.d.ts +5 -1
  17. package/dist/components/ChatDialog.svelte +4 -2
  18. package/dist/components/ChatDialog.svelte.d.ts +2 -0
  19. package/dist/components/DevTools.svelte +3 -1
  20. package/dist/components/DevTools.svelte.d.ts +4 -1
  21. package/dist/components/ForgotPasswordPage.svelte +5 -8
  22. package/dist/components/Header.svelte +5 -3
  23. package/dist/components/Header.svelte.d.ts +2 -0
  24. package/dist/components/Layout.svelte +10 -1
  25. package/dist/components/LoginPage.svelte +5 -8
  26. package/dist/components/RegisterPage.svelte +5 -8
  27. package/dist/components/SettingsPage.svelte +14 -2
  28. package/dist/components/Sidebar.svelte +10 -4
  29. package/dist/components/SvadminLogo.svelte +16 -0
  30. package/dist/components/SvadminLogo.svelte.d.ts +7 -0
  31. package/dist/components/SystemPageShell.svelte +34 -0
  32. package/dist/components/SystemPageShell.svelte.d.ts +8 -0
  33. package/dist/components/UpdatePasswordPage.svelte +5 -8
  34. package/dist/components/account/CompanyProfilePage.svelte +56 -1
  35. package/dist/components/account/ImportMembersPage.svelte +31 -0
  36. package/dist/components/account/SettingsEnterprisePage.svelte +172 -19
  37. package/dist/components/account/SettingsPlainPage.svelte +121 -17
  38. package/dist/components/account/SettingsSidebarPage.svelte +282 -70
  39. package/dist/components/account/TeamMembersPage.svelte +95 -4
  40. package/dist/components/account/UserProfilePage.svelte +206 -0
  41. package/dist/components/account/UserProfilePage.svelte.d.ts +4 -0
  42. package/dist/components/network/TeamCrewTablePage.svelte +37 -5
  43. package/dist/components/network/UserCardsNFTPage.svelte +26 -16
  44. package/dist/components/profile/ActivityTimeline.svelte +24 -2
  45. package/dist/components/profile/ActivityTimeline.svelte.d.ts +1 -0
  46. package/dist/components/profile/ProfileVariantSections.svelte +452 -0
  47. package/dist/components/profile/ProfileVariantSections.svelte.d.ts +8 -0
  48. package/dist/components/profile/ProjectsGrid.svelte +38 -8
  49. package/dist/components/profile/ProjectsGrid.svelte.d.ts +1 -0
  50. package/dist/components/profile/PublicProfilePage.svelte +47 -26
  51. package/dist/components/profile/PublicProfilePage.svelte.d.ts +1 -0
  52. package/dist/components/profile/TeamsShowcase.svelte +31 -12
  53. package/dist/components/profile/TeamsShowcase.svelte.d.ts +1 -0
  54. package/dist/components/ui/alert/alert.svelte.d.ts +14 -14
  55. package/dist/components/ui/avatar/avatar.svelte.d.ts +14 -14
  56. package/dist/components/ui/badge/badge.svelte.d.ts +20 -20
  57. package/dist/components/ui/button/button.svelte.d.ts +44 -44
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.js +3 -0
  60. package/dist/router-state.svelte.js +4 -0
  61. package/package.json +15 -15
@@ -2,99 +2,311 @@
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
3
  import * as Card from '../ui/card/index.js';
4
4
  import { Button } from '../ui/button/index.js';
5
+ import { Badge } from '../ui/badge/index.js';
6
+ import { Input } from '../ui/input/index.js';
5
7
  import { Label } from '../ui/label/index.js';
6
8
  import { Switch } from '../ui/switch/index.js';
7
9
  import { Separator } from '../ui/separator/index.js';
8
- import { Loader2, PanelLeft } from '@lucide/svelte';
10
+ import { Loader2, Camera, Key, Copy, Check } from '@lucide/svelte';
9
11
 
10
12
  const i18n = useTranslation();
11
13
 
12
- let defaultCollapsed = $state(false);
13
- let pinSidebar = $state(true);
14
- let showLabels = $state(true);
15
- let showIcons = $state(true);
16
- let density = $state<'compact' | 'standard'>('standard');
14
+ const navItems = [
15
+ { id: 'basic', label: 'account.basicSettings' },
16
+ { id: 'signin', label: 'account.socialSignIn' },
17
+ { id: 'password', label: 'account.password' },
18
+ { id: 'sso', label: 'account.singleSignOn' },
19
+ { id: 'preferences', label: 'account.preferences' },
20
+ { id: 'address', label: 'account.address' },
21
+ { id: 'api', label: 'account.manageApi' },
22
+ { id: 'delete', label: 'account.deleteAccount' },
23
+ ];
24
+
25
+ let activeSection = $state('basic');
26
+
27
+ function scrollTo(id: string) {
28
+ activeSection = id;
29
+ if (typeof document !== 'undefined') {
30
+ document.getElementById(`settings-section-${id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
31
+ }
32
+ }
33
+
34
+ let displayName = $state('Alex Chen');
35
+ let company = $state('Nebula Labs');
36
+ let phone = $state('+86 138 **** 9527');
37
+ let email = $state('alex@example.com');
38
+ let availableToHire = $state(true);
39
+ let profileVisible = $state(true);
40
+ let socialGithub = $state(true);
41
+ let socialGoogle = $state(false);
42
+ let twoFactor = $state(true);
43
+ let language = $state('en');
44
+ let timezone = $state('Asia/Shanghai');
45
+ let currency = $state('CNY');
46
+ let apiKey = $state('sv_demo_pub_9f3e••••41ab');
47
+ let copied = $state(false);
17
48
  let saving = $state(false);
18
49
 
19
50
  async function handleSave() {
20
51
  saving = true;
21
- await new Promise(r => setTimeout(r, 800));
52
+ await new Promise((r) => setTimeout(r, 800));
22
53
  saving = false;
23
54
  }
55
+
56
+ async function copyApiKey() {
57
+ try {
58
+ await navigator.clipboard.writeText(apiKey);
59
+ copied = true;
60
+ setTimeout(() => (copied = false), 2000);
61
+ } catch {
62
+ copied = false;
63
+ }
64
+ }
24
65
  </script>
25
66
 
26
67
  <div class="space-y-6" data-svadmin-content-page="account">
27
- <div>
28
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.settingsSidebar')}</h2>
29
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.settingsSidebarDescription')}</p>
68
+ <div class="flex items-start justify-between gap-4">
69
+ <div>
70
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.settingsSidebar')}</h2>
71
+ <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.settingsSidebarDescription')}</p>
72
+ </div>
73
+ <Button onclick={handleSave} disabled={saving} size="sm">
74
+ {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
75
+ {i18n.t('common.save')}
76
+ </Button>
30
77
  </div>
31
78
 
32
- <Card.Card class="border-border/60">
33
- <Card.CardHeader>
34
- <Card.CardTitle class="flex items-center gap-2 text-base">
35
- <PanelLeft class="h-4 w-4 text-muted-foreground" />
36
- {i18n.t('account.settingsSidebar')}
37
- </Card.CardTitle>
38
- </Card.CardHeader>
39
- <Card.CardContent class="space-y-4">
40
- <div class="flex items-center justify-between gap-4">
41
- <div class="space-y-1">
42
- <Label for="default-collapsed">默认折叠</Label>
43
- <p class="text-xs text-muted-foreground">启动时自动折叠侧边栏</p>
44
- </div>
45
- <Switch id="default-collapsed" bind:checked={defaultCollapsed} />
79
+ <div class="flex flex-col gap-6 lg:flex-row">
80
+ <!-- Anchor nav -->
81
+ <nav class="w-full shrink-0 lg:w-52">
82
+ <div class="flex gap-1 overflow-x-auto lg:sticky lg:top-6 lg:flex-col">
83
+ {#each navItems as item (item.id)}
84
+ <button
85
+ class="whitespace-nowrap rounded-lg px-3 py-2 text-left text-sm transition-colors
86
+ {activeSection === item.id ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
87
+ onclick={() => scrollTo(item.id)}
88
+ >
89
+ {i18n.t(item.label)}
90
+ </button>
91
+ {/each}
46
92
  </div>
93
+ </nav>
47
94
 
48
- <Separator />
95
+ <div class="min-w-0 flex-1 space-y-6">
96
+ <!-- Basic Settings -->
97
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-basic">
98
+ <Card.CardHeader>
99
+ <Card.CardTitle class="text-base">{i18n.t('account.basicSettings')}</Card.CardTitle>
100
+ </Card.CardHeader>
101
+ <Card.CardContent class="space-y-4">
102
+ <div class="flex items-center gap-4">
103
+ <div class="relative">
104
+ <div class="flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 text-primary text-xl font-bold">AC</div>
105
+ <button class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm">
106
+ <Camera class="h-3 w-3" />
107
+ </button>
108
+ </div>
109
+ <div>
110
+ <p class="text-sm font-medium text-foreground">{i18n.t('account.photo')}</p>
111
+ <p class="text-xs text-muted-foreground">150x150px JPEG, PNG Image</p>
112
+ </div>
113
+ </div>
114
+ <Separator />
115
+ <div class="grid gap-4 sm:grid-cols-2">
116
+ <div class="space-y-2">
117
+ <Label for="sb-name">{i18n.t('profile.name')}</Label>
118
+ <Input id="sb-name" bind:value={displayName} />
119
+ </div>
120
+ <div class="space-y-2">
121
+ <Label for="sb-company">{i18n.t('profile.company')}</Label>
122
+ <Input id="sb-company" bind:value={company} />
123
+ </div>
124
+ <div class="space-y-2">
125
+ <Label for="sb-phone">{i18n.t('account.phone')}</Label>
126
+ <Input id="sb-phone" bind:value={phone} />
127
+ </div>
128
+ <div class="space-y-2">
129
+ <Label for="sb-email">Email</Label>
130
+ <Input id="sb-email" bind:value={email} type="email" />
131
+ </div>
132
+ </div>
133
+ <Separator />
134
+ <div class="flex items-center justify-between gap-4">
135
+ <div class="space-y-1">
136
+ <Label for="sb-visibility">{i18n.t('account.visibility')}</Label>
137
+ <p class="text-xs text-muted-foreground">{i18n.t('account.publicProfileCard')}</p>
138
+ </div>
139
+ <Switch id="sb-visibility" bind:checked={profileVisible} />
140
+ </div>
141
+ <div class="flex items-center justify-between gap-4">
142
+ <div class="space-y-1">
143
+ <Label for="sb-availability">{i18n.t('account.availability')}</Label>
144
+ <p class="text-xs text-muted-foreground">{i18n.t('account.availableToHire')}</p>
145
+ </div>
146
+ <Switch id="sb-availability" bind:checked={availableToHire} />
147
+ </div>
148
+ </Card.CardContent>
149
+ </Card.Card>
49
150
 
50
- <div class="flex items-center justify-between gap-4">
51
- <div class="space-y-1">
52
- <Label for="pin-sidebar">{i18n.t('settings.general')}</Label>
53
- <p class="text-xs text-muted-foreground">固定侧边栏,防止自动收起</p>
54
- </div>
55
- <Switch id="pin-sidebar" bind:checked={pinSidebar} />
56
- </div>
151
+ <!-- Social Sign in + 2FA -->
152
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-signin">
153
+ <Card.CardHeader>
154
+ <Card.CardTitle class="text-base">{i18n.t('account.socialSignIn')}</Card.CardTitle>
155
+ </Card.CardHeader>
156
+ <Card.CardContent class="space-y-4">
157
+ <div class="flex items-center justify-between gap-4">
158
+ <div class="space-y-1">
159
+ <Label for="sb-github">Source Control</Label>
160
+ <p class="text-xs text-muted-foreground">alexchen</p>
161
+ </div>
162
+ <Switch id="sb-github" bind:checked={socialGithub} />
163
+ </div>
164
+ <div class="flex items-center justify-between gap-4">
165
+ <div class="space-y-1">
166
+ <Label for="sb-google">Identity Cloud</Label>
167
+ <p class="text-xs text-muted-foreground">alex@example.com</p>
168
+ </div>
169
+ <Switch id="sb-google" bind:checked={socialGoogle} />
170
+ </div>
171
+ <Separator />
172
+ <div class="flex items-center justify-between gap-4">
173
+ <div class="space-y-1">
174
+ <Label for="sb-2fa">{i18n.t('account.twoFactorAuth')}</Label>
175
+ <p class="text-xs text-muted-foreground">{i18n.t('security.twoFactorDescription')}</p>
176
+ </div>
177
+ <div class="flex items-center gap-2">
178
+ <Badge variant="secondary" class="text-[10px]">{twoFactor ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}</Badge>
179
+ <Switch id="sb-2fa" bind:checked={twoFactor} />
180
+ </div>
181
+ </div>
182
+ </Card.CardContent>
183
+ </Card.Card>
57
184
 
58
- <div class="flex items-center justify-between gap-4">
59
- <div class="space-y-1">
60
- <Label for="show-labels">显示标签文字</Label>
61
- <p class="text-xs text-muted-foreground">在图标旁显示菜单项名称</p>
62
- </div>
63
- <Switch id="show-labels" bind:checked={showLabels} />
64
- </div>
185
+ <!-- Password -->
186
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-password">
187
+ <Card.CardHeader>
188
+ <Card.CardTitle class="text-base">{i18n.t('account.password')}</Card.CardTitle>
189
+ </Card.CardHeader>
190
+ <Card.CardContent>
191
+ <div class="grid gap-4 sm:grid-cols-3">
192
+ <div class="space-y-2">
193
+ <Label for="sb-current-pw">{i18n.t('profile.currentPassword')}</Label>
194
+ <Input id="sb-current-pw" type="password" />
195
+ </div>
196
+ <div class="space-y-2">
197
+ <Label for="sb-new-pw">{i18n.t('profile.newPassword')}</Label>
198
+ <Input id="sb-new-pw" type="password" />
199
+ </div>
200
+ <div class="space-y-2">
201
+ <Label for="sb-confirm-pw">{i18n.t('auth.confirmPassword')}</Label>
202
+ <Input id="sb-confirm-pw" type="password" />
203
+ </div>
204
+ </div>
205
+ </Card.CardContent>
206
+ </Card.Card>
65
207
 
66
- <div class="flex items-center justify-between gap-4">
67
- <div class="space-y-1">
68
- <Label for="show-icons">显示图标</Label>
69
- <p class="text-xs text-muted-foreground">在标签旁显示菜单图标</p>
70
- </div>
71
- <Switch id="show-icons" bind:checked={showIcons} />
72
- </div>
208
+ <!-- SSO -->
209
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-sso">
210
+ <Card.CardHeader>
211
+ <Card.CardTitle class="text-base">{i18n.t('account.singleSignOn')}</Card.CardTitle>
212
+ </Card.CardHeader>
213
+ <Card.CardContent>
214
+ <div class="grid gap-4 sm:grid-cols-2">
215
+ <div class="space-y-2">
216
+ <Label for="sb-client-id">Client ID</Label>
217
+ <Input id="sb-client-id" placeholder="xxxxxxxx-xxxx-xxxx" />
218
+ </div>
219
+ <div class="space-y-2">
220
+ <Label for="sb-client-secret">Client Secret</Label>
221
+ <Input id="sb-client-secret" type="password" placeholder="••••••••" />
222
+ </div>
223
+ </div>
224
+ </Card.CardContent>
225
+ </Card.Card>
73
226
 
74
- <Separator />
227
+ <!-- Preferences -->
228
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-preferences">
229
+ <Card.CardHeader>
230
+ <Card.CardTitle class="text-base">{i18n.t('account.preferences')}</Card.CardTitle>
231
+ </Card.CardHeader>
232
+ <Card.CardContent>
233
+ <div class="grid gap-4 sm:grid-cols-3">
234
+ <div class="space-y-2">
235
+ <Label for="sb-language">{i18n.t('account.language')}</Label>
236
+ <select id="sb-language" bind:value={language} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
237
+ <option value="en">English</option>
238
+ <option value="zh-CN">中文(简体)</option>
239
+ </select>
240
+ </div>
241
+ <div class="space-y-2">
242
+ <Label for="sb-timezone">{i18n.t('account.timezone')}</Label>
243
+ <select id="sb-timezone" bind:value={timezone} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
244
+ <option value="Asia/Shanghai">Asia/Shanghai (GMT+8)</option>
245
+ <option value="America/Los_Angeles">America/Los_Angeles (GMT-7)</option>
246
+ <option value="Europe/Berlin">Europe/Berlin (GMT+2)</option>
247
+ </select>
248
+ </div>
249
+ <div class="space-y-2">
250
+ <Label for="sb-currency">{i18n.t('account.currency')}</Label>
251
+ <select id="sb-currency" bind:value={currency} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
252
+ <option value="CNY">CNY (¥)</option>
253
+ <option value="USD">USD ($)</option>
254
+ <option value="EUR">EUR (€)</option>
255
+ </select>
256
+ </div>
257
+ </div>
258
+ </Card.CardContent>
259
+ </Card.Card>
75
260
 
76
- <div class="space-y-2">
77
- <Label>{i18n.t('settings.sidebarDensity')}</Label>
78
- <div class="flex rounded-lg border border-input overflow-hidden">
79
- <button
80
- class="flex-1 px-4 py-2 text-sm font-medium transition-colors
81
- {density === 'compact' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
82
- onclick={() => density = 'compact'}
83
- >{i18n.t('settings.compact')}</button>
84
- <button
85
- class="flex-1 px-4 py-2 text-sm font-medium transition-colors
86
- {density === 'standard' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
87
- onclick={() => density = 'standard'}
88
- >{i18n.t('settings.standard')}</button>
89
- </div>
90
- </div>
261
+ <!-- Address -->
262
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-address">
263
+ <Card.CardHeader>
264
+ <Card.CardTitle class="text-base">{i18n.t('account.address')}</Card.CardTitle>
265
+ </Card.CardHeader>
266
+ <Card.CardContent>
267
+ <div class="grid gap-4 sm:grid-cols-2">
268
+ <div class="space-y-2">
269
+ <Label for="sb-country">{i18n.t('account.country')}</Label>
270
+ <Input id="sb-country" value="China" />
271
+ </div>
272
+ <div class="space-y-2">
273
+ <Label for="sb-city">{i18n.t('account.city')}</Label>
274
+ <Input id="sb-city" value="Shanghai" />
275
+ </div>
276
+ </div>
277
+ </Card.CardContent>
278
+ </Card.Card>
91
279
 
92
- <div class="flex justify-end">
93
- <Button onclick={handleSave} disabled={saving}>
94
- {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
95
- {i18n.t('common.save')}
96
- </Button>
97
- </div>
98
- </Card.CardContent>
99
- </Card.Card>
280
+ <!-- Manage API -->
281
+ <Card.Card class="border-border/60 scroll-mt-6" id="settings-section-api">
282
+ <Card.CardHeader>
283
+ <Card.CardTitle class="flex items-center gap-2 text-base">
284
+ <Key class="h-4 w-4 text-muted-foreground" />{i18n.t('account.manageApi')}
285
+ </Card.CardTitle>
286
+ </Card.CardHeader>
287
+ <Card.CardContent>
288
+ <div class="flex items-center gap-2">
289
+ <Input readonly value={apiKey} class="h-9 font-mono text-xs" />
290
+ <Button size="sm" variant="outline" onclick={copyApiKey}>
291
+ {#if copied}<Check class="h-4 w-4 text-green-500" />{:else}<Copy class="h-4 w-4" />{/if}
292
+ </Button>
293
+ <Button size="sm" variant="ghost">{i18n.t('api.regenerate')}</Button>
294
+ </div>
295
+ </Card.CardContent>
296
+ </Card.Card>
297
+
298
+ <!-- Delete Account -->
299
+ <Card.Card class="border-destructive/30 scroll-mt-6" id="settings-section-delete">
300
+ <Card.CardHeader>
301
+ <Card.CardTitle class="text-base text-destructive">{i18n.t('account.deleteAccount')}</Card.CardTitle>
302
+ <Card.CardDescription>{i18n.t('account.deleteAccountDescription')}</Card.CardDescription>
303
+ </Card.CardHeader>
304
+ <Card.CardContent>
305
+ <div class="flex justify-end">
306
+ <Button variant="destructive" size="sm">{i18n.t('account.deleteAccount')}</Button>
307
+ </div>
308
+ </Card.CardContent>
309
+ </Card.Card>
310
+ </div>
311
+ </div>
100
312
  </div>
@@ -3,7 +3,8 @@
3
3
  import * as Card from '../ui/card/index.js';
4
4
  import { Button } from '../ui/button/index.js';
5
5
  import { Input } from '../ui/input/index.js';
6
- import { Search, UserPlus, MoreHorizontal } from '@lucide/svelte';
6
+ import { Label } from '../ui/label/index.js';
7
+ import { Search, UserPlus, MoreHorizontal, Link2, Copy, Send } from '@lucide/svelte';
7
8
 
8
9
  const i18n = useTranslation();
9
10
 
@@ -19,6 +20,20 @@
19
20
  }
20
21
 
21
22
  let searchQuery = $state('');
23
+ let inviteEmail = $state('');
24
+ let inviteRole = $state('Viewer');
25
+ let linkCopied = $state(false);
26
+ let inviteStatus = $state('');
27
+
28
+ function focusInvite() {
29
+ document.getElementById('invite-email')?.focus();
30
+ }
31
+
32
+ function copyInviteLink() {
33
+ navigator.clipboard?.writeText('https://acme.com/invite/xK7fQ2').catch(() => {});
34
+ linkCopied = true;
35
+ setTimeout(() => linkCopied = false, 2000);
36
+ }
22
37
 
23
38
  let members = $state<TeamMember[]>([
24
39
  { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: 'Just now' },
@@ -27,8 +42,33 @@
27
42
  { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', department: 'Engineering', status: 'active', lastActive: '1 hour ago' },
28
43
  { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', department: 'Sales', status: 'inactive', lastActive: '2 weeks ago' },
29
44
  { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: '10 min ago' },
45
+ { id: '7', name: 'James Wilson', email: 'james@acme.com', role: 'Editor', department: 'Infrastructure', status: 'active', lastActive: '30 min ago' },
46
+ { id: '8', name: 'Yuki Tanaka', email: 'yuki@acme.com', role: 'Editor', department: 'Engineering', status: 'invited', lastActive: 'Pending' },
47
+ { id: '9', name: 'Priya Nair', email: 'priya@acme.com', role: 'Viewer', department: 'Data', status: 'active', lastActive: '2 hours ago' },
48
+ { id: '10', name: 'Carlos Mendez', email: 'carlos@acme.com', role: 'Editor', department: 'Engineering', status: 'active', lastActive: '1 day ago' },
49
+ { id: '11', name: 'Hana Sato', email: 'hana@acme.com', role: 'Viewer', department: 'Design', status: 'active', lastActive: '3 hours ago' },
50
+ { id: '12', name: 'Oscar Lund', email: 'oscar@acme.com', role: 'Viewer', department: 'Infrastructure', status: 'inactive', lastActive: '1 month ago' },
51
+ { id: '13', name: 'Mia Torres', email: 'mia@acme.com', role: 'Editor', department: 'Marketing', status: 'invited', lastActive: 'Pending' },
52
+ { id: '14', name: 'Chen Wei', email: 'chen@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: '15 min ago' },
30
53
  ]);
31
54
 
55
+ function sendInvite() {
56
+ const email = inviteEmail.trim();
57
+ if (!email) return;
58
+ const name = email.split('@')[0].split(/[._-]/).filter(Boolean).map(part => part[0]?.toUpperCase() + part.slice(1)).join(' ') || email;
59
+ members = [...members, {
60
+ id: `invite-${Date.now()}`,
61
+ name,
62
+ email,
63
+ role: inviteRole as TeamMember['role'],
64
+ department: 'Pending assignment',
65
+ status: 'invited',
66
+ lastActive: 'Pending',
67
+ }];
68
+ inviteStatus = email;
69
+ inviteEmail = '';
70
+ }
71
+
32
72
  const filtered = $derived(
33
73
  searchQuery
34
74
  ? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
@@ -59,10 +99,10 @@
59
99
  <div class="space-y-6" data-svadmin-content-page="account">
60
100
  <div class="flex items-start justify-between gap-4">
61
101
  <div>
62
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.teamMembers')}</h2>
102
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.teamMembersCount', { count: members.length })}</h2>
63
103
  <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.teamMembersDescription')}</p>
64
104
  </div>
65
- <Button size="sm">
105
+ <Button size="sm" onclick={focusInvite}>
66
106
  <UserPlus class="h-4 w-4 mr-1" />{i18n.t('account.inviteMember')}
67
107
  </Button>
68
108
  </div>
@@ -72,6 +112,57 @@
72
112
  <Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
73
113
  </div>
74
114
 
115
+ <!-- Invite people -->
116
+ <div class="grid gap-4 lg:grid-cols-2">
117
+ <Card.Card class="border-border/60">
118
+ <Card.CardHeader class="pb-3">
119
+ <Card.CardTitle class="text-base">{i18n.t('account.invitePeople')}</Card.CardTitle>
120
+ <Card.CardDescription>{i18n.t('account.invitePeopleDescription')}</Card.CardDescription>
121
+ </Card.CardHeader>
122
+ <Card.CardContent class="space-y-3">
123
+ <div class="grid gap-3 sm:grid-cols-[1fr_140px]">
124
+ <div class="space-y-1.5">
125
+ <Label for="invite-email">Email</Label>
126
+ <Input id="invite-email" type="email" placeholder="name@example.com" bind:value={inviteEmail} />
127
+ </div>
128
+ <div class="space-y-1.5">
129
+ <Label for="invite-role">{i18n.t('account.role')}</Label>
130
+ <select id="invite-role" bind:value={inviteRole} 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">
131
+ <option>Admin</option>
132
+ <option>Editor</option>
133
+ <option>Viewer</option>
134
+ </select>
135
+ </div>
136
+ </div>
137
+ <div class="flex justify-end">
138
+ <Button size="sm" disabled={!inviteEmail} onclick={sendInvite}>
139
+ <Send class="h-3.5 w-3.5 mr-1" />{i18n.t('account.sendInvite')}
140
+ </Button>
141
+ </div>
142
+ {#if inviteStatus}
143
+ <p class="text-xs text-green-600" role="status">{i18n.t('account.invited')}: {inviteStatus}</p>
144
+ {/if}
145
+ </Card.CardContent>
146
+ </Card.Card>
147
+
148
+ <Card.Card class="border-border/60">
149
+ <Card.CardHeader class="pb-3">
150
+ <Card.CardTitle class="flex items-center gap-2 text-base">
151
+ <Link2 class="h-4 w-4 text-muted-foreground" />{i18n.t('account.inviteWithLink')}
152
+ </Card.CardTitle>
153
+ <Card.CardDescription>{i18n.t('account.inviteLinkHint')}</Card.CardDescription>
154
+ </Card.CardHeader>
155
+ <Card.CardContent class="space-y-3">
156
+ <div class="flex items-center gap-2">
157
+ <Input readonly value="https://acme.com/invite/xK7fQ2" class="flex-1 font-mono text-xs" />
158
+ <Button size="sm" variant="outline" onclick={copyInviteLink}>
159
+ <Copy class="h-3.5 w-3.5 mr-1" />{linkCopied ? '✓' : ''} {i18n.t('account.copyLink')}
160
+ </Button>
161
+ </div>
162
+ </Card.CardContent>
163
+ </Card.Card>
164
+ </div>
165
+
75
166
  <!-- Members grid -->
76
167
  <div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
77
168
  {#each filtered as member (member.id)}
@@ -97,7 +188,7 @@
97
188
  <span class="text-xs text-muted-foreground">{member.lastActive}</span>
98
189
  </div>
99
190
  </div>
100
- <Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
191
+ <Button variant="ghost" size="icon-sm" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
101
192
  </div>
102
193
  </Card.CardContent>
103
194
  </Card.Card>