@svadmin/ui 0.34.9 → 0.35.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.
- package/dist/app.css +372 -0
- package/dist/component-registry.svelte.d.ts +1 -0
- package/dist/component-registry.svelte.js +1 -0
- package/dist/components/AICommandBar.svelte +3 -3
- package/dist/components/AboutSettings.svelte +2 -2
- package/dist/components/AdminApp.svelte +96 -18
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/AnomalyBadge.svelte +1 -1
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AppearanceSettings.svelte +5 -6
- package/dist/components/AuditLogDrawer.svelte +1 -1
- package/dist/components/AuditLogViewer.svelte +1 -1
- package/dist/components/AutoForm.svelte +50 -44
- package/dist/components/AutoTable.svelte +44 -39
- package/dist/components/Breadcrumbs.svelte +1 -0
- package/dist/components/ChatDialog.svelte +5 -4
- package/dist/components/ComboboxField.svelte +3 -3
- package/dist/components/CommandPalette.svelte +3 -3
- package/dist/components/CopilotPanel.svelte +2 -2
- package/dist/components/CreatePage.svelte +0 -1
- package/dist/components/DevTools.svelte +4 -3
- package/dist/components/DraggableHeader.svelte +2 -1
- package/dist/components/EditPage.svelte +1 -1
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +9 -8
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +7 -7
- package/dist/components/InferencerPanel.svelte +4 -3
- package/dist/components/InfiniteList.svelte +3 -4
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/KeyboardShortcuts.svelte +3 -3
- package/dist/components/Layout.svelte +13 -13
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/MarkdownRenderer.svelte +3 -5
- package/dist/components/ModalForm.svelte +1 -3
- package/dist/components/NotificationsSettings.svelte +115 -0
- package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +9 -7
- package/dist/components/PageSkeleton.svelte +4 -4
- package/dist/components/PasswordInput.svelte +1 -2
- package/dist/components/PermissionMatrix.svelte +5 -5
- package/dist/components/ProfilePage.svelte +1 -1
- package/dist/components/RegisterPage.svelte +103 -65
- package/dist/components/ResourceOperationsPage.svelte +153 -0
- package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
- package/dist/components/RolesSettings.svelte +1 -2
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +121 -55
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +13 -15
- package/dist/components/Sidebar.svelte +107 -58
- package/dist/components/SidebarItem.svelte +38 -14
- package/dist/components/StepsForm.svelte +1 -1
- package/dist/components/TaskList.svelte +1 -1
- package/dist/components/TaskQueueDrawer.svelte +2 -1
- package/dist/components/TenantSwitcher.svelte +1 -2
- package/dist/components/Toast.svelte +1 -0
- package/dist/components/TooltipButton.svelte +1 -0
- package/dist/components/TwoFactorAuthPage.svelte +247 -0
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
- package/dist/components/UpdatePasswordPage.svelte +86 -48
- package/dist/components/VirtualTable.svelte +1 -1
- package/dist/components/VoiceInput.svelte +1 -0
- package/dist/components/account/CompanyProfilePage.svelte +99 -0
- package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
- package/dist/components/account/GetStartedPage.svelte +123 -0
- package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
- package/dist/components/account/ImportMembersPage.svelte +143 -0
- package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
- package/dist/components/account/MembersStarterPage.svelte +104 -0
- package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
- package/dist/components/account/SecurityLogPage.svelte +108 -0
- package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
- package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsPlainPage.svelte +81 -0
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsSidebarPage.svelte +98 -0
- package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
- package/dist/components/account/TeamMembersPage.svelte +98 -0
- package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
- package/dist/components/buttons/ImportButton.svelte +1 -1
- package/dist/components/buttons/ListButton.svelte +1 -1
- package/dist/components/charts/BarChart.svelte +1 -1
- package/dist/components/charts/LineChart.svelte +3 -3
- package/dist/components/charts/PieChart.svelte +2 -2
- package/dist/components/fieldComponentMap.js +2 -1
- package/dist/components/fields/ArrayField.svelte +1 -1
- package/dist/components/fields/MarkdownField.svelte +1 -0
- package/dist/components/fields/MultiSelectField.svelte +1 -1
- package/dist/components/fields/TagField.svelte +1 -1
- package/dist/components/network/TeamCrewTablePage.svelte +122 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/components/ui/alert/index.js +1 -0
- package/dist/components/ui/badge/index.js +1 -0
- package/dist/components/ui/button/button.svelte +1 -0
- package/dist/components/ui/button/index.js +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +1 -1
- package/dist/components/ui/sheet/sheet.svelte +1 -0
- package/dist/components/ui/tabs/tabs.svelte +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/router-state.svelte.js +17 -0
- package/package.json +13 -13
|
@@ -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">
|
|
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,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">
|
|
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>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<div class="w-full overflow-x-auto {className}">
|
|
25
25
|
<svg viewBox="0 0 {totalWidth} {height + 30}" class="w-full" style="min-width: {Math.min(totalWidth, 300)}px;">
|
|
26
26
|
<!-- Bars -->
|
|
27
|
-
{#each data as point, i}
|
|
27
|
+
{#each data as point, i (i)}
|
|
28
28
|
{@const barHeight = (point.value / maxValue) * (height - 20)}
|
|
29
29
|
{@const x = 20 + i * (barWidth + gap)}
|
|
30
30
|
{@const y = height - barHeight}
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<div class="w-full overflow-x-auto {className}">
|
|
47
47
|
<svg viewBox="0 0 {chartWidth} {height}" class="w-full" style="min-width: {Math.min(chartWidth, 300)}px;">
|
|
48
48
|
<!-- Grid lines -->
|
|
49
|
-
{#each [0, 0.25, 0.5, 0.75, 1] as ratio}
|
|
49
|
+
{#each [0, 0.25, 0.5, 0.75, 1] as ratio, _i (_i)}
|
|
50
50
|
{@const y = padding.top + chartHeight * (1 - ratio)}
|
|
51
51
|
<line
|
|
52
52
|
x1={padding.left}
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
<!-- Dots -->
|
|
90
90
|
{#if showDots}
|
|
91
|
-
{#each data as point, i}
|
|
91
|
+
{#each data as point, i (i)}
|
|
92
92
|
<circle
|
|
93
93
|
cx={getX(i)}
|
|
94
94
|
cy={getY(point.value)}
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
{/if}
|
|
103
103
|
|
|
104
104
|
<!-- X-axis labels -->
|
|
105
|
-
{#each data as point, i}
|
|
105
|
+
{#each data as point, i (i)}
|
|
106
106
|
<text
|
|
107
107
|
x={getX(i)}
|
|
108
108
|
y={height - 4}
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
<div class="inline-flex flex-col items-center gap-3 {className}">
|
|
61
61
|
<svg viewBox="0 0 {size} {size}" width={size} height={size}>
|
|
62
|
-
{#each slices as slice, i}
|
|
62
|
+
{#each slices as slice, i (i)}
|
|
63
63
|
<path
|
|
64
64
|
d={getArcPath(slice.startAngle, slice.endAngle, radius, innerRadius)}
|
|
65
65
|
fill={slice.color}
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
|
|
85
85
|
<!-- Legend -->
|
|
86
86
|
<div class="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
|
87
|
-
{#each slices as slice}
|
|
87
|
+
{#each slices as slice, _i (_i)}
|
|
88
88
|
<div class="flex items-center gap-1.5 text-xs">
|
|
89
89
|
<span class="h-2.5 w-2.5 rounded-full" style="background-color: {slice.color};"></span>
|
|
90
90
|
<span class="text-muted-foreground">{slice.label}</span>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
// Shared field type → display component mapping
|
|
2
3
|
// Used by both ShowPage and FieldRenderer to avoid duplicate if/else chains.
|
|
3
4
|
// Components are lazily resolved by field type at runtime.
|
|
4
5
|
// Default display component map (read-only field display)
|
|
5
6
|
// Dynamically imports components only when first used
|
|
6
|
-
const
|
|
7
|
+
const _displayCache = new Map();
|
|
7
8
|
// Pre-import all field display components (tree-shaken by bundler)
|
|
8
9
|
import BooleanField from './fields/BooleanField.svelte';
|
|
9
10
|
import ImageField from './fields/ImageField.svelte';
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
61
|
<div class="space-y-4 pt-2">
|
|
62
|
-
{#each arrayVal as item, i}
|
|
62
|
+
{#each arrayVal as item, i (i)}
|
|
63
63
|
<div class="relative rounded-md border bg-card p-4 pt-6 shadow-sm">
|
|
64
64
|
<div class="absolute right-2 top-2">
|
|
65
65
|
<TooltipButton tooltip={t('common.remove') || 'Remove'} variant="ghost" size="icon" class="h-8 w-8 text-muted-foreground hover:text-destructive transition-colors" onclick={() => handleRemove(i)}>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
{#if items.length > 0}
|
|
13
13
|
<div class="flex flex-wrap gap-1">
|
|
14
|
-
{#each items as v}
|
|
14
|
+
{#each items as v, _i (_i)}
|
|
15
15
|
{@const label = options?.find((o: { label: string; value: string | number }) => o.value === v)?.label ?? String(v)}
|
|
16
16
|
<Badge variant="secondary" class="font-normal">{label}</Badge>
|
|
17
17
|
{/each}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const tags = $derived(Array.isArray(value) ? value : value ? [value] : []);
|
|
6
6
|
</script>
|
|
7
7
|
<span class="inline-flex flex-wrap gap-1">
|
|
8
|
-
{#each tags as tag}
|
|
8
|
+
{#each tags as tag, _i (_i)}
|
|
9
9
|
<Badge variant="secondary">{tag}</Badge>
|
|
10
10
|
{/each}
|
|
11
11
|
{#if tags.length === 0}<span class="text-muted-foreground">—</span>{/if}
|
|
@@ -0,0 +1,122 @@
|
|
|
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">
|
|
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="overflow-x-auto">
|
|
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
|
+
</Card.CardContent>
|
|
121
|
+
</Card.Card>
|
|
122
|
+
</div>
|
|
@@ -0,0 +1,134 @@
|
|
|
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 { Input } from '../ui/input/index.js';
|
|
7
|
+
import { Search, ExternalLink, Check, Plus } from '@lucide/svelte';
|
|
8
|
+
|
|
9
|
+
interface NFTUser {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
username: string;
|
|
13
|
+
avatar?: string;
|
|
14
|
+
bio: string;
|
|
15
|
+
collectionCount: number;
|
|
16
|
+
floorPrice: string;
|
|
17
|
+
volume: string;
|
|
18
|
+
verified: boolean;
|
|
19
|
+
tags: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let searchQuery = $state('');
|
|
23
|
+
|
|
24
|
+
const users: NFTUser[] = [
|
|
25
|
+
{ id: '1', name: 'CryptoArt Studio', username: '@cryptoart', bio: 'Digital art collection featuring generative abstracts', collectionCount: 42, floorPrice: '0.5 ETH', volume: '128.3 ETH', verified: true, tags: ['Art', 'Generative'] },
|
|
26
|
+
{ id: '2', name: 'PixelPunk Collective', username: '@pixelpunk', bio: 'On-chain pixel art characters with unique traits', collectionCount: 18, floorPrice: '1.2 ETH', volume: '342.7 ETH', verified: true, tags: ['PFP', 'Pixel'] },
|
|
27
|
+
{ id: '3', name: 'MetaVerse Builders', username: '@metabuild', bio: '3D architectural models for virtual worlds', collectionCount: 7, floorPrice: '0.8 ETH', volume: '56.1 ETH', verified: false, tags: ['3D', 'Virtual'] },
|
|
28
|
+
{ id: '4', name: 'SoundWave NFT', username: '@soundwave', bio: 'Music-backed NFTs with on-chain audio', collectionCount: 23, floorPrice: '0.3 ETH', volume: '89.4 ETH', verified: true, tags: ['Music', 'Audio'] },
|
|
29
|
+
{ id: '5', name: 'GameAssets DAO', username: '@gameassets', bio: 'In-game items and collectibles for Web3 gaming', collectionCount: 156, floorPrice: '0.05 ETH', volume: '412.8 ETH', verified: true, tags: ['Gaming', 'Items'] },
|
|
30
|
+
{ id: '6', name: 'PhotoVerse', username: '@photoverse', bio: 'Curated photography NFTs from world artists', collectionCount: 31, floorPrice: '0.15 ETH', volume: '67.2 ETH', verified: false, tags: ['Photo', 'Curated'] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const filtered = $derived(
|
|
34
|
+
searchQuery
|
|
35
|
+
? users.filter(u => u.name.toLowerCase().includes(searchQuery.toLowerCase()) || u.username.toLowerCase().includes(searchQuery.toLowerCase()) || u.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
|
36
|
+
: users
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
40
|
+
|
|
41
|
+
let connectedIds = $state<string[]>([]);
|
|
42
|
+
|
|
43
|
+
function toggleConnect(id: string) {
|
|
44
|
+
connectedIds = connectedIds.includes(id)
|
|
45
|
+
? connectedIds.filter(connectedId => connectedId !== id)
|
|
46
|
+
: [...connectedIds, id];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isConnected(id: string): boolean {
|
|
50
|
+
return connectedIds.includes(id);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="space-y-6">
|
|
55
|
+
<div>
|
|
56
|
+
<h2 class="text-xl font-semibold text-foreground">{t('network.userCards')}</h2>
|
|
57
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('network.userCardsDescription')}</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="relative">
|
|
61
|
+
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
62
|
+
<Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
66
|
+
{#each filtered as user (user.id)}
|
|
67
|
+
<Card.Card class="overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
|
|
68
|
+
<div class="h-20 bg-gradient-to-r from-violet-500/20 via-purple-500/15 to-fuchsia-500/20 relative">
|
|
69
|
+
<div class="absolute inset-0 opacity-[0.08]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 16px 16px;"></div>
|
|
70
|
+
</div>
|
|
71
|
+
<Card.CardContent class="relative px-4 pb-4">
|
|
72
|
+
<div class="-mt-8 mb-2 flex items-end gap-3">
|
|
73
|
+
<div class="ring-3 ring-card rounded-xl overflow-hidden shrink-0">
|
|
74
|
+
<div class="flex h-14 w-14 items-center justify-center bg-primary/10 text-primary font-bold">
|
|
75
|
+
{initials(user.name)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1 min-w-0 pb-0.5">
|
|
79
|
+
<div class="flex items-center gap-1.5">
|
|
80
|
+
<h4 class="text-sm font-semibold text-foreground truncate">{user.name}</h4>
|
|
81
|
+
{#if user.verified}
|
|
82
|
+
<Check class="h-3.5 w-3.5 text-blue-500" />
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
85
|
+
<p class="text-xs text-muted-foreground">{user.username}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<p class="text-xs text-muted-foreground line-clamp-2 mb-3">{user.bio}</p>
|
|
90
|
+
|
|
91
|
+
<div class="flex flex-wrap gap-1 mb-3">
|
|
92
|
+
{#each user.tags as tag (tag)}
|
|
93
|
+
<Badge variant="outline" class="text-[10px]">{tag}</Badge>
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- NFT stats -->
|
|
98
|
+
<div class="grid grid-cols-3 gap-2 border-t pt-3">
|
|
99
|
+
<div class="text-center">
|
|
100
|
+
<div class="text-sm font-semibold text-foreground">{user.collectionCount}</div>
|
|
101
|
+
<div class="text-[10px] text-muted-foreground">{t('network.nftCollection')}</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="text-center">
|
|
104
|
+
<div class="text-sm font-semibold text-foreground">{user.floorPrice}</div>
|
|
105
|
+
<div class="text-[10px] text-muted-foreground">{t('network.floorPrice')}</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="text-center">
|
|
108
|
+
<div class="text-xs font-semibold text-foreground">{user.volume}</div>
|
|
109
|
+
<div class="text-[10px] text-muted-foreground">{t('network.volume')}</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="mt-3 flex gap-2">
|
|
114
|
+
<Button
|
|
115
|
+
size="sm"
|
|
116
|
+
variant={isConnected(user.id) ? 'outline' : 'default'}
|
|
117
|
+
class="flex-1"
|
|
118
|
+
onclick={() => toggleConnect(user.id)}
|
|
119
|
+
>
|
|
120
|
+
{#if isConnected(user.id)}
|
|
121
|
+
<Check class="h-3.5 w-3.5 mr-1" />{t('network.connected')}
|
|
122
|
+
{:else}
|
|
123
|
+
<Plus class="h-3.5 w-3.5 mr-1" />{t('network.connect')}
|
|
124
|
+
{/if}
|
|
125
|
+
</Button>
|
|
126
|
+
<Button size="sm" variant="ghost">
|
|
127
|
+
<ExternalLink class="h-3.5 w-3.5" />
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</Card.CardContent>
|
|
131
|
+
</Card.Card>
|
|
132
|
+
{/each}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|