@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.
Files changed (49) hide show
  1. package/dist/components/AuthPageShell.svelte +32 -0
  2. package/dist/components/AuthPageShell.svelte.d.ts +11 -0
  3. package/dist/components/ForgotPasswordPage.svelte +6 -74
  4. package/dist/components/LoginPage.svelte +34 -125
  5. package/dist/components/PermissionMatrix.svelte +5 -5
  6. package/dist/components/ProfilePage.svelte +4 -23
  7. package/dist/components/RegisterPage.svelte +7 -62
  8. package/dist/components/SecuritySettings.svelte +4 -10
  9. package/dist/components/SvadminLogo.svelte +1 -1
  10. package/dist/components/Toast.svelte +7 -5
  11. package/dist/components/UpdatePasswordPage.svelte +3 -55
  12. package/dist/components/account/CompanyProfilePage.svelte +11 -5
  13. package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
  14. package/dist/components/account/SettingsPlainPage.svelte +25 -3
  15. package/dist/components/account/SettingsSidebarPage.svelte +31 -2
  16. package/dist/components/content/ApiKeyList.svelte +13 -4
  17. package/dist/components/content/ApiKeyList.svelte.d.ts +7 -0
  18. package/dist/components/content/DataState.svelte +25 -9
  19. package/dist/components/content/DataState.svelte.d.ts +2 -0
  20. package/dist/components/content/FeedbackNotice.svelte +61 -0
  21. package/dist/components/content/FeedbackNotice.svelte.d.ts +13 -0
  22. package/dist/components/content/FileList.svelte +12 -4
  23. package/dist/components/content/FileList.svelte.d.ts +9 -0
  24. package/dist/components/content/FilterToolbar.svelte +10 -2
  25. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
  26. package/dist/components/content/MemberList.svelte +13 -5
  27. package/dist/components/content/MemberList.svelte.d.ts +9 -0
  28. package/dist/components/content/MetricBlock.svelte +19 -2
  29. package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
  30. package/dist/components/content/NetworkTable.svelte +9 -3
  31. package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
  32. package/dist/components/content/NetworkUserCard.svelte +47 -7
  33. package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
  34. package/dist/components/content/SectionHeader.svelte +3 -2
  35. package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
  36. package/dist/components/content/SecurityEventTable.svelte +14 -5
  37. package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
  38. package/dist/components/content/SettingsFieldRow.svelte +28 -0
  39. package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
  40. package/dist/components/content/SettingsGroup.svelte +33 -0
  41. package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
  42. package/dist/components/content/WorkspaceLayout.svelte +28 -0
  43. package/dist/components/content/WorkspaceLayout.svelte.d.ts +11 -0
  44. package/dist/components/content/index.d.ts +6 -1
  45. package/dist/components/content/index.js +4 -0
  46. package/dist/components/network/UserCardsNFTPage.svelte +25 -6
  47. package/dist/index.d.ts +8 -1
  48. package/dist/index.js +5 -0
  49. 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 { query = $bindable(''), placeholder = 'Search', filters, actions, class: className = '' }: Props = $props();
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="Clear search" onclick={() => query = ''}><X class="size-3.5" /></Button>{/if}
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}
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  query?: string;
4
4
  placeholder?: string;
5
+ clearLabel?: string;
5
6
  filters?: Snippet;
6
7
  actions?: Snippet;
7
8
  class?: string;
@@ -1,16 +1,24 @@
1
1
  <script lang="ts">
2
- import { MoreHorizontal, UserRound } from '@lucide/svelte';
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
- let { members = [], onaction, class: className = '' }: Props = $props();
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 ?? 'Team member'}</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={'Actions for ' + member.name} onclick={() => onaction?.(member)}><MoreHorizontal class="size-4" /></Button>{/if}</div>
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 { label, value, detail, trend, icon, loading = false, class: className = '' }: Props = $props();
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="font-medium text-success">{trend}</span>{/if}{#if detail}<span class="text-muted-foreground">{detail}</span>{/if}</div>{/if}
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 { MessageSquare, UserPlus } from '@lucide/svelte';
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 Props { user: NetworkUser; onconnect?: () => void; onmessage?: () => void; class?: string; }
8
- let { user, onconnect, onmessage, class: className = '' }: Props = $props();
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="flex items-start gap-3 p-4">
13
- <Avatar src={user.avatar} alt={user.name} fallback={initials} size="lg" />
14
- <div class="min-w-0 flex-1"><h3 class="truncate text-sm font-semibold text-foreground">{user.name}</h3>{#if user.handle}<p class="truncate text-xs text-muted-foreground">{user.handle}</p>{/if}{#if user.role}<p class="mt-2 text-xs font-medium text-foreground">{user.role}</p>{/if}{#if user.summary}<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{user.summary}</p>{/if}</div>
15
- <div class="flex shrink-0 gap-1">{#if onmessage}<Button variant="ghost" size="icon-sm" aria-label={'Message ' + user.name} onclick={onmessage}><MessageSquare class="size-4" /></Button>{/if}{#if onconnect}<Button variant="outline" size="icon-sm" aria-label={'Connect with ' + user.name} onclick={onconnect}><UserPlus class="size-4" /></Button>{/if}</div>
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}
@@ -3,6 +3,7 @@ interface Props {
3
3
  title: string;
4
4
  description?: string;
5
5
  actions?: Snippet;
6
+ id?: string;
6
7
  class?: string;
7
8
  }
8
9
  declare const SectionHeader: import("svelte").Component<Props, {}, "">;
@@ -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
- let { events = [], class: className = '' }: Props = $props();
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}<DataState state="empty" title={i18n.t('network.userCards')} />{:else}<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{#each filtered as user (user.id)}<Card.Card><Card.CardContent class="space-y-4 p-4"><div class="flex items-start gap-3"><span class="flex size-10 items-center justify-center rounded-md bg-muted text-sm font-semibold text-muted-foreground">{user.name.slice(0, 2).toUpperCase()}</span><div class="min-w-0 flex-1"><div class="flex items-center gap-1.5"><h2 class="truncate text-sm font-semibold text-foreground">{user.name}</h2>{#if user.verified}<Check class="size-3.5 text-primary" />{/if}</div><p class="text-xs text-muted-foreground">{user.handle}</p></div></div><p class="text-sm text-muted-foreground">{user.summary}</p><div class="flex flex-wrap gap-1.5">{#each user.tags as tag (tag)}<Badge variant="outline">{tag}</Badge>{/each}</div><div class="grid grid-cols-3 gap-2 border-t border-border pt-3 text-center"><div><p class="text-sm font-semibold text-foreground">{user.collectionCount}</p><p class="text-xs text-muted-foreground">{i18n.t('network.nftCollection')}</p></div><div><p class="text-sm font-semibold text-foreground">{user.floorPrice}</p><p class="text-xs text-muted-foreground">{i18n.t('network.floorPrice')}</p></div><div><p class="text-sm font-semibold text-foreground">{user.volume}</p><p class="text-xs text-muted-foreground">{i18n.t('network.volume')}</p></div></div><Button size="sm" variant={connected.includes(user.id) ? 'outline' : 'default'} class="w-full" onclick={() => toggle(user.id)}>{#if connected.includes(user.id)}<Check class="size-3.5" />{i18n.t('network.connected')}{:else}<Plus class="size-3.5" />{i18n.t('network.connect')}{/if}</Button></Card.CardContent></Card.Card>{/each}</div>{/if}
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>