@svadmin/ui 0.42.3 → 0.44.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 (79) 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/ResourceOperationsPage.svelte +84 -223
  5. package/dist/components/ResourceOperationsPage.svelte.d.ts +1 -1
  6. package/dist/components/TwoFactorAuthPage.svelte +44 -232
  7. package/dist/components/account/CompanyProfilePage.svelte +21 -142
  8. package/dist/components/account/GetStartedPage.svelte +45 -106
  9. package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
  10. package/dist/components/account/MembersStarterPage.svelte +49 -92
  11. package/dist/components/account/SecurityLogPage.svelte +16 -131
  12. package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
  13. package/dist/components/account/SettingsPlainPage.svelte +10 -175
  14. package/dist/components/account/SettingsSidebarPage.svelte +11 -304
  15. package/dist/components/account/TeamMembersPage.svelte +18 -187
  16. package/dist/components/account/UserProfilePage.svelte +19 -196
  17. package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
  18. package/dist/components/content/ApiKeyList.svelte +13 -0
  19. package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
  20. package/dist/components/content/ContentPageHeader.svelte +31 -0
  21. package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
  22. package/dist/components/content/ContentPageShell.svelte +39 -0
  23. package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
  24. package/dist/components/content/DataState.svelte +44 -0
  25. package/dist/components/content/DataState.svelte.d.ts +13 -0
  26. package/dist/components/content/DescriptionList.svelte +27 -0
  27. package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
  28. package/dist/components/content/FileList.svelte +13 -0
  29. package/dist/components/content/FileList.svelte.d.ts +16 -0
  30. package/dist/components/content/FilterToolbar.svelte +24 -0
  31. package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
  32. package/dist/components/content/ImportDropzone.svelte +19 -0
  33. package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
  34. package/dist/components/content/IntegrationCard.svelte +16 -0
  35. package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
  36. package/dist/components/content/MemberList.svelte +16 -0
  37. package/dist/components/content/MemberList.svelte.d.ts +17 -0
  38. package/dist/components/content/MetricBlock.svelte +27 -0
  39. package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
  40. package/dist/components/content/NetworkTable.svelte +18 -0
  41. package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
  42. package/dist/components/content/NetworkTable.types.d.ts +4 -0
  43. package/dist/components/content/NetworkTable.types.js +1 -0
  44. package/dist/components/content/NetworkUserCard.svelte +17 -0
  45. package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
  46. package/dist/components/content/OtpInput.svelte +22 -0
  47. package/dist/components/content/OtpInput.svelte.d.ts +10 -0
  48. package/dist/components/content/PageToolbar.svelte +18 -0
  49. package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
  50. package/dist/components/content/ProjectCard.svelte +39 -0
  51. package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
  52. package/dist/components/content/SectionHeader.svelte +18 -0
  53. package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
  54. package/dist/components/content/SecurityEventTable.svelte +18 -0
  55. package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
  56. package/dist/components/content/StatusBadge.svelte +19 -0
  57. package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
  58. package/dist/components/content/SystemErrorState.svelte +18 -0
  59. package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
  60. package/dist/components/content/TeamCard.svelte +26 -0
  61. package/dist/components/content/TeamCard.svelte.d.ts +23 -0
  62. package/dist/components/content/TwoFactorStepper.svelte +11 -0
  63. package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
  64. package/dist/components/content/index.d.ts +32 -0
  65. package/dist/components/content/index.js +22 -0
  66. package/dist/components/network/TeamCrewTablePage.svelte +29 -181
  67. package/dist/components/network/UserCardsNFTPage.svelte +24 -137
  68. package/dist/components/profile/ActivityTimeline.svelte +17 -91
  69. package/dist/components/profile/ProfileCard.svelte +4 -4
  70. package/dist/components/profile/ProjectsGrid.svelte +28 -106
  71. package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
  72. package/dist/components/profile/PublicProfilePage.svelte +34 -186
  73. package/dist/components/profile/TeamsShowcase.svelte +12 -114
  74. package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
  75. package/dist/index.d.ts +36 -0
  76. package/dist/index.js +24 -0
  77. package/dist/reference-data.d.ts +171 -0
  78. package/dist/reference-data.js +47 -0
  79. package/package.json +2 -2
@@ -1,312 +1,19 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import * as Card from '../ui/card/index.js';
4
3
  import { Button } from '../ui/button/index.js';
5
- import { Badge } from '../ui/badge/index.js';
6
4
  import { Input } from '../ui/input/index.js';
7
- import { Label } from '../ui/label/index.js';
8
5
  import { Switch } from '../ui/switch/index.js';
9
- import { Separator } from '../ui/separator/index.js';
10
- import { Loader2, Camera, Key, Copy, Check } from '@lucide/svelte';
11
-
6
+ import * as Card from '../ui/card/index.js';
7
+ import ContentPageShell from '../content/ContentPageShell.svelte';
8
+ import ContentPageHeader from '../content/ContentPageHeader.svelte';
12
9
  const i18n = useTranslation();
13
-
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);
48
- let saving = $state(false);
49
-
50
- async function handleSave() {
51
- saving = true;
52
- await new Promise((r) => setTimeout(r, 800));
53
- saving = false;
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
- }
10
+ const sections = [{ id: 'profile', title: i18n.t('account.basicSettings') }, { id: 'signin', title: i18n.t('account.socialSignIn') }, { id: 'preferences', title: i18n.t('account.preferences') }, { id: 'api', title: i18n.t('account.manageApi') }];
11
+ let active = $state('profile');
12
+ let visible = $state(true);
13
+ let available = $state(true);
65
14
  </script>
66
15
 
67
- <div class="space-y-6" data-svadmin-content-page="account">
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>
77
- </div>
78
-
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}
92
- </div>
93
- </nav>
94
-
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>
150
-
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>
184
-
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>
207
-
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>
226
-
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>
260
-
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>
279
-
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>
312
- </div>
16
+ <ContentPageShell pageId="account-settings-sidebar" width="wide">
17
+ <ContentPageHeader title={i18n.t('account.settingsSidebar')} description={i18n.t('account.settingsSidebarDescription')} />
18
+ <div class="grid gap-6 lg:grid-cols-[12rem_minmax(0,1fr)]"><nav class="flex gap-1 overflow-x-auto lg:flex-col">{#each sections as section (section.id)}<button type="button" class={'whitespace-nowrap rounded-md px-3 py-2 text-left text-sm ' + (active === section.id ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground hover:text-foreground')} onclick={() => active = section.id}>{section.title}</button>{/each}</nav><div class="space-y-4">{#if active === 'profile'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.basicSettings')}</h2><Input value="Alex Chen" aria-label={i18n.t('profile.name')} /><Input value="Nebula Labs" aria-label="Company" /><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.visibility')}</span><Switch bind:checked={visible} /></div><div class="flex items-center justify-between"><span class="text-sm text-foreground">{i18n.t('account.availability')}</span><Switch bind:checked={available} /></div></Card.CardContent></Card.Card>{:else if active === 'signin'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.socialSignIn')}</h2><p class="text-sm text-muted-foreground">Connect the providers your team trusts for sign in.</p><Button variant="outline">GitHub</Button><Button variant="outline">Google</Button></Card.CardContent></Card.Card>{:else if active === 'preferences'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.preferences')}</h2><Input value="English" aria-label={i18n.t('account.language')} /><Input value="Asia/Shanghai" aria-label={i18n.t('account.timezone')} /><Input value="CNY" aria-label={i18n.t('account.currency')} /></Card.CardContent></Card.Card>{:else}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.manageApi')}</h2><Input readonly value="sv_demo_pub_9f3e********41ab" aria-label="API key" /><Button variant="outline">{i18n.t('common.copy')}</Button></Card.CardContent></Card.Card>{/if}</div></div>
19
+ </ContentPageShell>
@@ -1,197 +1,28 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import * as Card from '../ui/card/index.js';
3
+ import { Copy, Link2, Send, UserPlus } 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 { Search, UserPlus, MoreHorizontal, Link2, Copy, Send } from '@lucide/svelte';
8
-
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 FilterToolbar from '../content/FilterToolbar.svelte';
11
+ import MemberList from '../content/MemberList.svelte';
12
+ import type { MemberSummary } from '../content/MemberList.svelte';
13
+ import { referenceDemoData } from '../../reference-data.js';
9
14
  const i18n = useTranslation();
10
-
11
- interface TeamMember {
12
- id: string;
13
- name: string;
14
- email: string;
15
- role: 'Admin' | 'Editor' | 'Viewer';
16
- avatar?: string;
17
- department: string;
18
- status: 'active' | 'invited' | 'inactive';
19
- lastActive: string;
20
- }
21
-
22
- let searchQuery = $state('');
15
+ let query = $state('');
23
16
  let inviteEmail = $state('');
24
- let inviteRole = $state('Viewer');
25
- let linkCopied = $state(false);
26
17
  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
- }
37
-
38
- let members = $state<TeamMember[]>([
39
- { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: 'Just now' },
40
- { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', department: 'Design', status: 'active', lastActive: '5 min ago' },
41
- { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', department: 'Marketing', status: 'invited', lastActive: 'Pending' },
42
- { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', department: 'Engineering', status: 'active', lastActive: '1 hour ago' },
43
- { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', department: 'Sales', status: 'inactive', lastActive: '2 weeks ago' },
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' },
53
- ]);
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
-
72
- const filtered = $derived(
73
- searchQuery
74
- ? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
75
- : members
76
- );
77
-
78
- const roleColors = {
79
- 'Admin': 'bg-red-500/10 text-red-600',
80
- 'Editor': 'bg-blue-500/10 text-blue-600',
81
- 'Viewer': 'bg-green-500/10 text-green-600',
82
- };
83
-
84
- const statusColors = {
85
- 'active': 'bg-green-500/10 text-green-600',
86
- 'invited': 'bg-amber-500/10 text-amber-600',
87
- 'inactive': 'bg-muted text-muted-foreground',
88
- };
89
-
90
- const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
91
-
92
- function statusLabel(status: TeamMember['status']) {
93
- if (status === 'active') return i18n.t('account.active');
94
- if (status === 'invited') return i18n.t('account.invited');
95
- return i18n.t('account.inactive');
96
- }
18
+ let members = $state<MemberSummary[]>(referenceDemoData.members.map((member) => ({ ...member, status: member.status === 'active' ? 'success' : member.status === 'invited' ? 'warning' : 'neutral' })));
19
+ const filtered = $derived(query ? members.filter((member) => `${member.name} ${member.email} ${member.department}`.toLowerCase().includes(query.toLowerCase())) : members);
20
+ function sendInvite() { const email = inviteEmail.trim(); if (!email) return; members = [...members, { id: `invite-${Date.now()}`, name: email.split('@')[0], email, role: 'Viewer', department: 'Pending assignment', status: 'warning' }]; inviteStatus = email; inviteEmail = ''; }
97
21
  </script>
98
22
 
99
- <div class="space-y-6" data-svadmin-content-page="account">
100
- <div class="flex items-start justify-between gap-4">
101
- <div>
102
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.teamMembersCount', { count: members.length })}</h2>
103
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.teamMembersDescription')}</p>
104
- </div>
105
- <Button size="sm" onclick={focusInvite}>
106
- <UserPlus class="h-4 w-4 mr-1" />{i18n.t('account.inviteMember')}
107
- </Button>
108
- </div>
109
-
110
- <div class="relative">
111
- <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
112
- <Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
113
- </div>
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
-
166
- <!-- Members grid -->
167
- <div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
168
- {#each filtered as member (member.id)}
169
- <Card.Card class="border-border/60 hover:border-primary/30 transition-colors">
170
- <Card.CardContent class="p-4">
171
- <div class="flex items-start gap-3">
172
- <div class="shrink-0">
173
- <div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold">
174
- {initials(member.name)}
175
- </div>
176
- </div>
177
- <div class="flex-1 min-w-0">
178
- <div class="flex items-center gap-2">
179
- <h4 class="text-sm font-semibold text-foreground truncate">{member.name}</h4>
180
- </div>
181
- <p class="text-xs text-muted-foreground truncate">{member.email}</p>
182
- <div class="mt-2 flex flex-wrap gap-1.5">
183
- <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {roleColors[member.role]}">{member.role}</span>
184
- <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {statusColors[member.status]}">{statusLabel(member.status)}</span>
185
- </div>
186
- <div class="mt-2 flex items-center justify-between">
187
- <span class="text-xs text-muted-foreground">{member.department}</span>
188
- <span class="text-xs text-muted-foreground">{member.lastActive}</span>
189
- </div>
190
- </div>
191
- <Button variant="ghost" size="icon-sm" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
192
- </div>
193
- </Card.CardContent>
194
- </Card.Card>
195
- {/each}
196
- </div>
197
- </div>
23
+ <ContentPageShell pageId="account-team-members" width="wide">
24
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t("account.teamMembers")} description={i18n.t("account.teamMembersDescription")} /><Button size="sm" onclick={() => document.getElementById("invite-email")?.focus()}><UserPlus class="size-3.5" />{i18n.t("account.inviteMember")}</Button></div>
25
+ <FilterToolbar bind:query placeholder={i18n.t('common.search')} />
26
+ <div class="grid gap-4 lg:grid-cols-2"><Card.Card><Card.CardContent class="space-y-3 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.invitePeople')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.invitePeopleDescription')}</p></div><div class="flex flex-col gap-2 sm:flex-row"><div class="min-w-0 flex-1 space-y-2"><Label for="invite-email">Email</Label><Input id="invite-email" bind:value={inviteEmail} type="email" placeholder="name@example.com" /></div><Button class="self-end" size="sm" disabled={!inviteEmail} onclick={sendInvite}><Send class="size-3.5" />{i18n.t('account.sendInvite')}</Button></div>{#if inviteStatus}<p class="text-xs text-success" role="status">{i18n.t('account.invited')}: {inviteStatus}</p>{/if}</Card.CardContent></Card.Card><Card.Card><Card.CardContent class="space-y-3 p-5"><div class="flex items-center gap-2"><Link2 class="size-4 text-muted-foreground" /><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.inviteWithLink')}</h2></div><p class="text-sm text-muted-foreground">{i18n.t('account.inviteLinkHint')}</p><div class="flex gap-2"><Input readonly value="https://acme.com/invite/xK7fQ2" class="font-mono text-xs" /><Button variant="outline" size="sm"><Copy class="size-3.5" />{i18n.t('account.copyLink')}</Button></div></Card.CardContent></Card.Card></div>
27
+ <MemberList members={filtered} />
28
+ </ContentPageShell>