@svadmin/ui 0.44.0 → 0.46.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/components/AuthPageShell.svelte +32 -0
- package/dist/components/AuthPageShell.svelte.d.ts +11 -0
- package/dist/components/ForgotPasswordPage.svelte +6 -74
- package/dist/components/LoginPage.svelte +34 -125
- package/dist/components/PermissionMatrix.svelte +5 -5
- package/dist/components/ProfilePage.svelte +4 -23
- package/dist/components/RegisterPage.svelte +7 -62
- package/dist/components/SecuritySettings.svelte +4 -10
- package/dist/components/SvadminLogo.svelte +1 -1
- package/dist/components/Toast.svelte +7 -5
- package/dist/components/UpdatePasswordPage.svelte +3 -55
- package/dist/components/account/CompanyProfilePage.svelte +11 -5
- package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
- package/dist/components/account/SettingsPlainPage.svelte +25 -3
- package/dist/components/account/SettingsSidebarPage.svelte +31 -2
- package/dist/components/content/ApiKeyList.svelte +13 -4
- package/dist/components/content/ApiKeyList.svelte.d.ts +7 -0
- package/dist/components/content/DataState.svelte +25 -9
- package/dist/components/content/DataState.svelte.d.ts +2 -0
- package/dist/components/content/FeedbackNotice.svelte +61 -0
- package/dist/components/content/FeedbackNotice.svelte.d.ts +13 -0
- package/dist/components/content/FileList.svelte +12 -4
- package/dist/components/content/FileList.svelte.d.ts +9 -0
- package/dist/components/content/FilterToolbar.svelte +10 -2
- package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
- package/dist/components/content/MemberList.svelte +13 -5
- package/dist/components/content/MemberList.svelte.d.ts +9 -0
- package/dist/components/content/MetricBlock.svelte +19 -2
- package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
- package/dist/components/content/NetworkTable.svelte +9 -3
- package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
- package/dist/components/content/NetworkUserCard.svelte +47 -7
- package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
- package/dist/components/content/SectionHeader.svelte +3 -2
- package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
- package/dist/components/content/SecurityEventTable.svelte +14 -5
- package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
- package/dist/components/content/SettingsFieldRow.svelte +28 -0
- package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
- package/dist/components/content/SettingsGroup.svelte +33 -0
- package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
- package/dist/components/content/WorkspaceLayout.svelte +28 -0
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +11 -0
- package/dist/components/content/index.d.ts +6 -1
- package/dist/components/content/index.js +4 -0
- package/dist/components/network/UserCardsNFTPage.svelte +25 -6
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -0
- package/package.json +2 -2
|
@@ -6,18 +6,26 @@
|
|
|
6
6
|
interface Props {
|
|
7
7
|
query?: string;
|
|
8
8
|
placeholder?: string;
|
|
9
|
+
clearLabel?: string;
|
|
9
10
|
filters?: Snippet;
|
|
10
11
|
actions?: Snippet;
|
|
11
12
|
class?: string;
|
|
12
13
|
}
|
|
13
|
-
let {
|
|
14
|
+
let {
|
|
15
|
+
query = $bindable(''),
|
|
16
|
+
placeholder = 'Search',
|
|
17
|
+
clearLabel = 'Clear search',
|
|
18
|
+
filters,
|
|
19
|
+
actions,
|
|
20
|
+
class: className = '',
|
|
21
|
+
}: Props = $props();
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
24
|
<div class={'flex flex-col gap-2 sm:flex-row sm:items-center ' + className}>
|
|
17
25
|
<div class="relative min-w-0 flex-1">
|
|
18
26
|
<Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
19
27
|
<Input bind:value={query} {placeholder} class="pl-9 pr-9" aria-label={placeholder} />
|
|
20
|
-
{#if query}<Button variant="ghost" size="icon-xs" class="absolute right-1 top-1/2 -translate-y-1/2" aria-label=
|
|
28
|
+
{#if query}<Button variant="ghost" size="icon-xs" class="absolute right-1 top-1/2 -translate-y-1/2" aria-label={clearLabel} title={clearLabel} onclick={() => query = ''}><X class="size-3.5" /></Button>{/if}
|
|
21
29
|
</div>
|
|
22
30
|
{#if filters}<div class="flex flex-wrap items-center gap-2">{@render filters()}</div>{/if}
|
|
23
31
|
{#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { MoreHorizontal
|
|
2
|
+
import { MoreHorizontal } from '@lucide/svelte';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
4
|
import { Avatar } from '../ui/avatar/index.js';
|
|
4
5
|
import { Button } from '../ui/button/index.js';
|
|
5
6
|
import StatusBadge from './StatusBadge.svelte';
|
|
7
|
+
import DataState from './DataState.svelte';
|
|
8
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
6
9
|
export interface MemberSummary { id: string; name: string; email?: string; role?: string; department?: string; status?: 'success' | 'warning' | 'danger' | 'info' | 'neutral'; avatar?: string; }
|
|
7
|
-
interface Props { members?: MemberSummary[]; onaction?: (member: MemberSummary) => void; class?: string; }
|
|
8
|
-
|
|
10
|
+
interface Props { members?: MemberSummary[]; state?: DataStateKind; stateTitle?: string; stateDescription?: string; emptyTitle?: string; emptyDescription?: string; retry?: () => void; retryLabel?: string; loadingLabel?: string; onaction?: (member: MemberSummary) => void; class?: string; }
|
|
11
|
+
const i18n = useTranslation();
|
|
12
|
+
let { members = [], state, stateTitle, stateDescription, emptyTitle, emptyDescription, retry, retryLabel, loadingLabel, onaction, class: className = '' }: Props = $props();
|
|
13
|
+
const resolvedState = $derived(state ?? (members.length === 0 ? 'empty' : undefined));
|
|
9
14
|
const initials = (name: string) => name.split(' ').map((part) => part[0]).join('').slice(0, 2).toUpperCase();
|
|
10
15
|
</script>
|
|
16
|
+
{#if resolvedState}
|
|
17
|
+
<DataState state={resolvedState} title={stateTitle ?? emptyTitle} description={stateDescription ?? emptyDescription} {retry} {retryLabel} {loadingLabel} class={className} />
|
|
18
|
+
{:else}
|
|
11
19
|
<div class={'divide-y divide-border overflow-hidden rounded-lg border border-border bg-card ' + className}>
|
|
12
20
|
{#each members as member (member.id)}
|
|
13
|
-
<div class="flex items-center gap-3 px-4 py-3"><Avatar src={member.avatar} alt={member.name} fallback={initials(member.name)} size="sm" /><div class="min-w-0 flex-1"><p class="truncate text-sm font-medium text-foreground">{member.name}</p><p class="truncate text-xs text-muted-foreground">{member.email ?? member.department ?? '
|
|
21
|
+
<div class="flex items-center gap-3 px-4 py-3"><Avatar src={member.avatar} alt={member.name} fallback={initials(member.name)} size="sm" /><div class="min-w-0 flex-1"><p class="truncate text-sm font-medium text-foreground">{member.name}</p><p class="truncate text-xs text-muted-foreground">{member.email ?? member.department ?? i18n.t('profileSections.members')}</p></div>{#if member.role}<span class="hidden text-xs text-muted-foreground sm:inline">{member.role}</span>{/if}{#if member.status}<StatusBadge status={member.status} label={member.status} />{/if}{#if onaction}<Button variant="ghost" size="icon-sm" aria-label={i18n.t('common.actions') + ' ' + member.name} onclick={() => onaction?.(member)}><MoreHorizontal class="size-4" /></Button>{/if}</div>
|
|
14
22
|
{/each}
|
|
15
|
-
{#if members.length === 0}<div class="flex flex-col items-center gap-2 p-6 text-center text-sm text-muted-foreground"><UserRound class="size-6" />No members</div>{/if}
|
|
16
23
|
</div>
|
|
24
|
+
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
1
2
|
export interface MemberSummary {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
@@ -9,6 +10,14 @@ export interface MemberSummary {
|
|
|
9
10
|
}
|
|
10
11
|
interface Props {
|
|
11
12
|
members?: MemberSummary[];
|
|
13
|
+
state?: DataStateKind;
|
|
14
|
+
stateTitle?: string;
|
|
15
|
+
stateDescription?: string;
|
|
16
|
+
emptyTitle?: string;
|
|
17
|
+
emptyDescription?: string;
|
|
18
|
+
retry?: () => void;
|
|
19
|
+
retryLabel?: string;
|
|
20
|
+
loadingLabel?: string;
|
|
12
21
|
onaction?: (member: MemberSummary) => void;
|
|
13
22
|
class?: string;
|
|
14
23
|
}
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { Skeleton } from '../ui/skeleton/index.js';
|
|
4
|
+
export type MetricTrendTone = 'positive' | 'negative' | 'warning' | 'neutral';
|
|
4
5
|
interface Props {
|
|
5
6
|
label: string;
|
|
6
7
|
value: string | number;
|
|
7
8
|
detail?: string;
|
|
8
9
|
trend?: string;
|
|
10
|
+
trendTone?: MetricTrendTone;
|
|
9
11
|
icon?: Snippet;
|
|
10
12
|
loading?: boolean;
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
13
|
-
let {
|
|
15
|
+
let {
|
|
16
|
+
label,
|
|
17
|
+
value,
|
|
18
|
+
detail,
|
|
19
|
+
trend,
|
|
20
|
+
trendTone = 'neutral',
|
|
21
|
+
icon,
|
|
22
|
+
loading = false,
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
const trendClass: Record<MetricTrendTone, string> = {
|
|
26
|
+
positive: 'text-success',
|
|
27
|
+
negative: 'text-destructive',
|
|
28
|
+
warning: 'text-warning-foreground',
|
|
29
|
+
neutral: 'text-muted-foreground',
|
|
30
|
+
};
|
|
14
31
|
</script>
|
|
15
32
|
|
|
16
33
|
<div data-svadmin-metric-card class={'min-w-0 rounded-lg border border-border bg-card p-4 shadow-sm ' + className}>
|
|
@@ -22,6 +39,6 @@
|
|
|
22
39
|
<Skeleton class="mt-3 h-8 w-24" />
|
|
23
40
|
{:else}
|
|
24
41
|
<p class="mt-2 truncate text-2xl font-semibold tracking-normal text-foreground">{value}</p>
|
|
25
|
-
{#if detail || trend}<div class="mt-2 flex flex-wrap gap-2 text-xs">{#if trend}<span class=
|
|
42
|
+
{#if detail || trend}<div class="mt-2 flex flex-wrap gap-2 text-xs">{#if trend}<span class={'font-medium ' + trendClass[trendTone]}>{trend}</span>{/if}{#if detail}<span class="text-muted-foreground">{detail}</span>{/if}</div>{/if}
|
|
26
43
|
{/if}
|
|
27
44
|
</div>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
export type MetricTrendTone = 'positive' | 'negative' | 'warning' | 'neutral';
|
|
2
3
|
interface Props {
|
|
3
4
|
label: string;
|
|
4
5
|
value: string | number;
|
|
5
6
|
detail?: string;
|
|
6
7
|
trend?: string;
|
|
8
|
+
trendTone?: MetricTrendTone;
|
|
7
9
|
icon?: Snippet;
|
|
8
10
|
loading?: boolean;
|
|
9
11
|
class?: string;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import * as Table from '../ui/table/index.js';
|
|
4
|
+
import DataState from './DataState.svelte';
|
|
5
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
4
6
|
import type { NetworkColumn } from './NetworkTable.types.js';
|
|
5
|
-
interface Props { rows?: T[]; columns: NetworkColumn<T>[]; row?: Snippet<[T]>; class?: string; }
|
|
6
|
-
let { rows = [], columns, row, class: className = '' }: Props = $props();
|
|
7
|
+
interface Props { rows?: T[]; columns: NetworkColumn<T>[]; row?: Snippet<[T]>; state?: DataStateKind; stateTitle?: string; stateDescription?: string; emptyTitle?: string; emptyDescription?: string; retry?: () => void; retryLabel?: string; loadingLabel?: string; class?: string; }
|
|
8
|
+
let { rows = [], columns, row, state, stateTitle, stateDescription, emptyTitle, emptyDescription, retry, retryLabel, loadingLabel, class: className = '' }: Props = $props();
|
|
9
|
+
const resolvedState = $derived(state ?? (rows.length === 0 ? 'empty' : undefined));
|
|
7
10
|
</script>
|
|
11
|
+
{#if resolvedState}
|
|
12
|
+
<DataState state={resolvedState} title={stateTitle ?? emptyTitle} description={stateDescription ?? emptyDescription} {retry} {retryLabel} {loadingLabel} class={className} />
|
|
13
|
+
{:else}
|
|
8
14
|
<div class={'overflow-x-auto rounded-lg border border-border bg-card ' + className}>
|
|
9
15
|
<Table.Root data-svadmin-datatable>
|
|
10
16
|
<Table.Header data-svadmin-table-head><Table.Row>{#each columns as column (String(column.key))}<Table.Head>{column.label}</Table.Head>{/each}</Table.Row></Table.Header>
|
|
@@ -14,5 +20,5 @@
|
|
|
14
20
|
{/each}
|
|
15
21
|
</Table.Body>
|
|
16
22
|
</Table.Root>
|
|
17
|
-
{#if rows.length === 0}<div class="p-6 text-center text-sm text-muted-foreground">No records</div>{/if}
|
|
18
23
|
</div>
|
|
24
|
+
{/if}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
2
3
|
import type { NetworkColumn } from './NetworkTable.types.js';
|
|
3
4
|
declare function $$render<T extends object>(): {
|
|
4
5
|
props: {
|
|
5
6
|
rows?: T[];
|
|
6
7
|
columns: NetworkColumn<T>[];
|
|
7
8
|
row?: Snippet<[T]>;
|
|
9
|
+
state?: DataStateKind;
|
|
10
|
+
stateTitle?: string;
|
|
11
|
+
stateDescription?: string;
|
|
12
|
+
emptyTitle?: string;
|
|
13
|
+
emptyDescription?: string;
|
|
14
|
+
retry?: () => void;
|
|
15
|
+
retryLabel?: string;
|
|
16
|
+
loadingLabel?: string;
|
|
8
17
|
class?: string;
|
|
9
18
|
};
|
|
10
19
|
exports: {};
|
|
@@ -1,17 +1,57 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { Check, MessageSquare, Plus } from '@lucide/svelte';
|
|
3
4
|
import { Avatar } from '../ui/avatar/index.js';
|
|
5
|
+
import { Badge } from '../ui/badge/index.js';
|
|
4
6
|
import { Button } from '../ui/button/index.js';
|
|
5
7
|
import * as Card from '../ui/card/index.js';
|
|
6
8
|
export interface NetworkUser { id: string; name: string; handle?: string; role?: string; avatar?: string; summary?: string; }
|
|
7
|
-
interface
|
|
8
|
-
|
|
9
|
+
export interface NetworkMetric { label: string; value: string | number; }
|
|
10
|
+
interface Props {
|
|
11
|
+
user: NetworkUser;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
metrics?: NetworkMetric[];
|
|
14
|
+
verified?: boolean;
|
|
15
|
+
verifiedLabel?: string;
|
|
16
|
+
connected?: boolean;
|
|
17
|
+
connectLabel?: string;
|
|
18
|
+
connectedLabel?: string;
|
|
19
|
+
onconnect?: () => void;
|
|
20
|
+
onmessage?: () => void;
|
|
21
|
+
footer?: Snippet;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
let {
|
|
25
|
+
user,
|
|
26
|
+
tags = [],
|
|
27
|
+
metrics = [],
|
|
28
|
+
verified = false,
|
|
29
|
+
verifiedLabel = 'Verified',
|
|
30
|
+
connected = false,
|
|
31
|
+
connectLabel = 'Connect',
|
|
32
|
+
connectedLabel = 'Connected',
|
|
33
|
+
onconnect,
|
|
34
|
+
onmessage,
|
|
35
|
+
footer,
|
|
36
|
+
class: className = '',
|
|
37
|
+
}: Props = $props();
|
|
9
38
|
const initials = $derived(user.name.split(' ').map((part) => part[0]).join('').slice(0, 2).toUpperCase());
|
|
10
39
|
</script>
|
|
11
40
|
<Card.Card class={className}>
|
|
12
|
-
<Card.CardContent class="
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
41
|
+
<Card.CardContent class="space-y-4 p-4">
|
|
42
|
+
<div class="flex items-start gap-3">
|
|
43
|
+
<Avatar src={user.avatar} alt={user.name} fallback={initials} size="lg" />
|
|
44
|
+
<div class="min-w-0 flex-1">
|
|
45
|
+
<div class="flex min-w-0 items-center gap-1.5"><h3 class="truncate text-sm font-semibold text-foreground">{user.name}</h3>{#if verified}<Check class="size-3.5 shrink-0 text-primary" aria-label={verifiedLabel} />{/if}</div>
|
|
46
|
+
{#if user.handle}<p class="truncate text-xs text-muted-foreground">{user.handle}</p>{/if}
|
|
47
|
+
{#if user.role}<p class="mt-2 text-xs font-medium text-foreground">{user.role}</p>{/if}
|
|
48
|
+
</div>
|
|
49
|
+
{#if onmessage}<Button variant="ghost" size="icon-sm" aria-label={'Message ' + user.name} onclick={onmessage}><MessageSquare class="size-4" /></Button>{/if}
|
|
50
|
+
</div>
|
|
51
|
+
{#if user.summary}<p class="text-sm leading-5 text-muted-foreground">{user.summary}</p>{/if}
|
|
52
|
+
{#if tags.length > 0}<div class="flex flex-wrap gap-1.5">{#each tags as tag (tag)}<Badge variant="outline">{tag}</Badge>{/each}</div>{/if}
|
|
53
|
+
{#if metrics.length > 0}<dl class="grid gap-2 border-t border-border pt-3 text-center" style:grid-template-columns={`repeat(${metrics.length}, minmax(0, 1fr))`}>{#each metrics as metric (metric.label)}<div class="min-w-0"><dd class="truncate text-sm font-semibold text-foreground">{metric.value}</dd><dt class="mt-1 truncate text-xs text-muted-foreground">{metric.label}</dt></div>{/each}</dl>{/if}
|
|
54
|
+
{#if onconnect}<Button size="sm" variant={connected ? 'outline' : 'default'} class="w-full" onclick={onconnect}>{#if connected}<Check class="size-3.5" />{connectedLabel}{:else}<Plus class="size-3.5" />{connectLabel}{/if}</Button>{/if}
|
|
55
|
+
{#if footer}<div class="border-t border-border pt-3">{@render footer()}</div>{/if}
|
|
16
56
|
</Card.CardContent>
|
|
17
57
|
</Card.Card>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
export interface NetworkUser {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
@@ -6,10 +7,22 @@ export interface NetworkUser {
|
|
|
6
7
|
avatar?: string;
|
|
7
8
|
summary?: string;
|
|
8
9
|
}
|
|
10
|
+
export interface NetworkMetric {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string | number;
|
|
13
|
+
}
|
|
9
14
|
interface Props {
|
|
10
15
|
user: NetworkUser;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
metrics?: NetworkMetric[];
|
|
18
|
+
verified?: boolean;
|
|
19
|
+
verifiedLabel?: string;
|
|
20
|
+
connected?: boolean;
|
|
21
|
+
connectLabel?: string;
|
|
22
|
+
connectedLabel?: string;
|
|
11
23
|
onconnect?: () => void;
|
|
12
24
|
onmessage?: () => void;
|
|
25
|
+
footer?: Snippet;
|
|
13
26
|
class?: string;
|
|
14
27
|
}
|
|
15
28
|
declare const NetworkUserCard: import("svelte").Component<Props, {}, "">;
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
title: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
actions?: Snippet;
|
|
7
|
+
id?: string;
|
|
7
8
|
class?: string;
|
|
8
9
|
}
|
|
9
|
-
let { title, description, actions, class: className = '' }: Props = $props();
|
|
10
|
+
let { title, description, actions, id, class: className = '' }: Props = $props();
|
|
10
11
|
</script>
|
|
11
12
|
|
|
12
13
|
<div class={'flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between ' + className}>
|
|
13
14
|
<div class="min-w-0 space-y-1">
|
|
14
|
-
<h2 class="text-base font-semibold text-foreground">{title}</h2>
|
|
15
|
+
<h2 {id} class="text-base font-semibold text-foreground">{title}</h2>
|
|
15
16
|
{#if description}<p class="text-sm text-muted-foreground">{description}</p>{/if}
|
|
16
17
|
</div>
|
|
17
18
|
{#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as Table from '../ui/table/index.js';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
4
|
import StatusBadge from './StatusBadge.svelte';
|
|
5
|
+
import DataState from './DataState.svelte';
|
|
6
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
4
7
|
export interface SecurityEvent { id: string; event: string; actor?: string; location?: string; createdAt: string; severity: 'info' | 'warning' | 'danger'; }
|
|
5
|
-
interface Props { events?: SecurityEvent[]; class?: string; }
|
|
6
|
-
|
|
8
|
+
interface Props { events?: SecurityEvent[]; state?: DataStateKind; stateTitle?: string; stateDescription?: string; emptyTitle?: string; emptyDescription?: string; retry?: () => void; retryLabel?: string; loadingLabel?: string; class?: string; }
|
|
9
|
+
const i18n = useTranslation();
|
|
10
|
+
let { events = [], state, stateTitle, stateDescription, emptyTitle, emptyDescription, retry, retryLabel, loadingLabel, class: className = '' }: Props = $props();
|
|
11
|
+
const resolvedState = $derived(state ?? (events.length === 0 ? 'empty' : undefined));
|
|
12
|
+
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
7
13
|
</script>
|
|
14
|
+
{#if resolvedState}
|
|
15
|
+
<DataState state={resolvedState} title={stateTitle ?? emptyTitle} description={stateDescription ?? emptyDescription} {retry} {retryLabel} {loadingLabel} class={className} />
|
|
16
|
+
{:else}
|
|
8
17
|
<div class={'overflow-x-auto rounded-lg border border-border bg-card ' + className}>
|
|
9
18
|
<Table.Root data-svadmin-datatable>
|
|
10
|
-
<Table.Header data-svadmin-table-head><Table.Row><Table.Head>Event</Table.Head><Table.Head>Actor</Table.Head><Table.Head>Location</Table.Head><Table.Head>Time</Table.Head><Table.Head>Risk</Table.Head></Table.Row></Table.Header>
|
|
19
|
+
<Table.Header data-svadmin-table-head><Table.Row><Table.Head>{isZh ? '事件' : 'Event'}</Table.Head><Table.Head>{isZh ? '操作者' : 'Actor'}</Table.Head><Table.Head>{isZh ? '位置' : 'Location'}</Table.Head><Table.Head>{isZh ? '时间' : 'Time'}</Table.Head><Table.Head>{isZh ? '风险' : 'Risk'}</Table.Head></Table.Row></Table.Header>
|
|
11
20
|
<Table.Body>
|
|
12
21
|
{#each events as event (event.id)}
|
|
13
|
-
<Table.Row data-svadmin-table-row><Table.Cell class="font-medium">{event.event}</Table.Cell><Table.Cell>{event.actor ?? 'System'}</Table.Cell><Table.Cell>{event.location ?? 'Unknown'}</Table.Cell><Table.Cell class="whitespace-nowrap">{event.createdAt}</Table.Cell><Table.Cell><StatusBadge status={event.severity} label={event.severity} /></Table.Cell></Table.Row>
|
|
22
|
+
<Table.Row data-svadmin-table-row><Table.Cell class="font-medium">{event.event}</Table.Cell><Table.Cell>{event.actor ?? (isZh ? '系统' : 'System')}</Table.Cell><Table.Cell>{event.location ?? (isZh ? '未知' : 'Unknown')}</Table.Cell><Table.Cell class="whitespace-nowrap">{event.createdAt}</Table.Cell><Table.Cell><StatusBadge status={event.severity} label={event.severity} /></Table.Cell></Table.Row>
|
|
14
23
|
{/each}
|
|
15
24
|
</Table.Body>
|
|
16
25
|
</Table.Root>
|
|
17
|
-
{#if events.length === 0}<div class="p-6 text-center text-sm text-muted-foreground">No security events</div>{/if}
|
|
18
26
|
</div>
|
|
27
|
+
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataStateKind } from './DataState.svelte';
|
|
1
2
|
export interface SecurityEvent {
|
|
2
3
|
id: string;
|
|
3
4
|
event: string;
|
|
@@ -8,6 +9,14 @@ export interface SecurityEvent {
|
|
|
8
9
|
}
|
|
9
10
|
interface Props {
|
|
10
11
|
events?: SecurityEvent[];
|
|
12
|
+
state?: DataStateKind;
|
|
13
|
+
stateTitle?: string;
|
|
14
|
+
stateDescription?: string;
|
|
15
|
+
emptyTitle?: string;
|
|
16
|
+
emptyDescription?: string;
|
|
17
|
+
retry?: () => void;
|
|
18
|
+
retryLabel?: string;
|
|
19
|
+
loadingLabel?: string;
|
|
11
20
|
class?: string;
|
|
12
21
|
}
|
|
13
22
|
declare const SecurityEventTable: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
control: Snippet;
|
|
9
|
+
separated?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
label,
|
|
15
|
+
description,
|
|
16
|
+
control,
|
|
17
|
+
separated = false,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={cn('flex min-h-11 items-center justify-between gap-4 py-3', separated && 'border-t border-border', className)} data-svadmin-settings-field-row>
|
|
23
|
+
<div class="min-w-0">
|
|
24
|
+
<p class="text-sm font-medium text-foreground">{label}</p>
|
|
25
|
+
{#if description}<p class="mt-1 max-w-2xl text-sm leading-5 text-muted-foreground">{description}</p>{/if}
|
|
26
|
+
</div>
|
|
27
|
+
<div class="shrink-0">{@render control()}</div>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
control: Snippet;
|
|
6
|
+
separated?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const SettingsFieldRow: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type SettingsFieldRow = ReturnType<typeof SettingsFieldRow>;
|
|
11
|
+
export default SettingsFieldRow;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
actions?: Snippet;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
class?: string;
|
|
11
|
+
bodyClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
actions,
|
|
18
|
+
children,
|
|
19
|
+
class: className = '',
|
|
20
|
+
bodyClass = '',
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<section class={cn('overflow-hidden rounded-lg border border-border bg-card shadow-sm', className)} data-svadmin-settings-group>
|
|
25
|
+
<header class="flex flex-col gap-3 border-b border-border px-5 py-4 sm:flex-row sm:items-start sm:justify-between">
|
|
26
|
+
<div class="min-w-0 space-y-1">
|
|
27
|
+
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
28
|
+
{#if description}<p class="max-w-2xl text-sm leading-6 text-muted-foreground">{description}</p>{/if}
|
|
29
|
+
</div>
|
|
30
|
+
{#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
|
|
31
|
+
</header>
|
|
32
|
+
<div class={cn('p-5', bodyClass)}>{@render children()}</div>
|
|
33
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
actions?: Snippet;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
class?: string;
|
|
8
|
+
bodyClass?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const SettingsGroup: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type SettingsGroup = ReturnType<typeof SettingsGroup>;
|
|
12
|
+
export default SettingsGroup;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
primary: Snippet;
|
|
7
|
+
secondary?: Snippet;
|
|
8
|
+
summary?: Snippet;
|
|
9
|
+
secondaryWidth?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
primary,
|
|
15
|
+
secondary,
|
|
16
|
+
summary,
|
|
17
|
+
secondaryWidth = '22rem',
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={cn('flex flex-col gap-6', className)} data-svadmin-workspace-layout style:--workspace-secondary-width={secondaryWidth}>
|
|
23
|
+
{#if summary}<div class="order-2 sm:order-1">{@render summary()}</div>{/if}
|
|
24
|
+
<div class="order-1 grid min-w-0 gap-6 sm:order-2 lg:grid-cols-[minmax(0,1fr)_var(--workspace-secondary-width)]">
|
|
25
|
+
<div class="min-w-0" data-svadmin-workspace-primary>{@render primary()}</div>
|
|
26
|
+
{#if secondary}<aside class="min-w-0">{@render secondary()}</aside>{/if}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
primary: Snippet;
|
|
4
|
+
secondary?: Snippet;
|
|
5
|
+
summary?: Snippet;
|
|
6
|
+
secondaryWidth?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const WorkspaceLayout: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type WorkspaceLayout = ReturnType<typeof WorkspaceLayout>;
|
|
11
|
+
export default WorkspaceLayout;
|
|
@@ -2,6 +2,9 @@ export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
|
2
2
|
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
|
+
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
|
+
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
5
8
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
6
9
|
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
7
10
|
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
@@ -24,9 +27,11 @@ export { default as MemberList } from './MemberList.svelte';
|
|
|
24
27
|
export type { MemberSummary } from './MemberList.svelte';
|
|
25
28
|
export { default as ImportDropzone } from './ImportDropzone.svelte';
|
|
26
29
|
export { default as NetworkUserCard } from './NetworkUserCard.svelte';
|
|
27
|
-
export type { NetworkUser } from './NetworkUserCard.svelte';
|
|
30
|
+
export type { NetworkUser, NetworkMetric } from './NetworkUserCard.svelte';
|
|
28
31
|
export { default as NetworkTable } from './NetworkTable.svelte';
|
|
29
32
|
export type { NetworkColumn } from './NetworkTable.types.js';
|
|
30
33
|
export { default as OtpInput } from './OtpInput.svelte';
|
|
31
34
|
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
32
35
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
36
|
+
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
37
|
+
export type { FeedbackNoticeTone, FeedbackNoticePriority } from './FeedbackNotice.svelte';
|
|
@@ -2,6 +2,9 @@ export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
|
2
2
|
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
|
+
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
|
+
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
5
8
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
6
9
|
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
7
10
|
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
@@ -20,3 +23,4 @@ export { default as NetworkTable } from './NetworkTable.svelte';
|
|
|
20
23
|
export { default as OtpInput } from './OtpInput.svelte';
|
|
21
24
|
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
22
25
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
26
|
+
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import { Check, Plus } from '@lucide/svelte';
|
|
4
|
-
import { Button } from '../ui/button/index.js';
|
|
5
|
-
import { Badge } from '../ui/badge/index.js';
|
|
6
|
-
import * as Card from '../ui/card/index.js';
|
|
7
3
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
8
4
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
5
|
import FilterToolbar from '../content/FilterToolbar.svelte';
|
|
10
6
|
import DataState from '../content/DataState.svelte';
|
|
7
|
+
import NetworkUserCard from '../content/NetworkUserCard.svelte';
|
|
11
8
|
import type { NetworkUser } from '../content/NetworkUserCard.svelte';
|
|
12
9
|
|
|
13
10
|
const i18n = useTranslation();
|
|
@@ -28,6 +25,28 @@
|
|
|
28
25
|
|
|
29
26
|
<ContentPageShell pageId="network-user-cards" width="wide">
|
|
30
27
|
<ContentPageHeader title={i18n.t('network.userCards')} description={i18n.t('network.userCardsDescription')} />
|
|
31
|
-
<FilterToolbar bind:query placeholder={i18n.t('common.search')} />
|
|
32
|
-
{#if filtered.length === 0}
|
|
28
|
+
<FilterToolbar bind:query placeholder={i18n.t('common.search')} clearLabel={i18n.locale === 'zh-CN' ? '清除搜索' : 'Clear search'} />
|
|
29
|
+
{#if filtered.length === 0}
|
|
30
|
+
<DataState state="empty" title={i18n.t('network.userCards')} />
|
|
31
|
+
{:else}
|
|
32
|
+
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
33
|
+
{#each filtered as user (user.id)}
|
|
34
|
+
<NetworkUserCard
|
|
35
|
+
{user}
|
|
36
|
+
tags={user.tags}
|
|
37
|
+
verified={user.verified}
|
|
38
|
+
verifiedLabel={i18n.locale === 'zh-CN' ? '已验证' : 'Verified'}
|
|
39
|
+
metrics={[
|
|
40
|
+
{ label: i18n.t('network.nftCollection'), value: user.collectionCount },
|
|
41
|
+
{ label: i18n.t('network.floorPrice'), value: user.floorPrice },
|
|
42
|
+
{ label: i18n.t('network.volume'), value: user.volume },
|
|
43
|
+
]}
|
|
44
|
+
connected={connected.includes(user.id)}
|
|
45
|
+
connectLabel={i18n.t('network.connect')}
|
|
46
|
+
connectedLabel={i18n.t('network.connected')}
|
|
47
|
+
onconnect={() => toggle(user.id)}
|
|
48
|
+
/>
|
|
49
|
+
{/each}
|
|
50
|
+
</div>
|
|
51
|
+
{/if}
|
|
33
52
|
</ContentPageShell>
|