@svadmin/ui 0.42.3 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/app.css +462 -825
  2. package/dist/app.theme.css +462 -825
  3. package/dist/components/ErrorPage.svelte +4 -56
  4. package/dist/components/ResourceOperationsPage.svelte +84 -223
  5. package/dist/components/ResourceOperationsPage.svelte.d.ts +1 -1
  6. package/dist/components/TwoFactorAuthPage.svelte +44 -232
  7. package/dist/components/account/CompanyProfilePage.svelte +21 -142
  8. package/dist/components/account/GetStartedPage.svelte +45 -106
  9. package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
  10. package/dist/components/account/MembersStarterPage.svelte +49 -92
  11. package/dist/components/account/SecurityLogPage.svelte +16 -131
  12. package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
  13. package/dist/components/account/SettingsPlainPage.svelte +10 -175
  14. package/dist/components/account/SettingsSidebarPage.svelte +11 -304
  15. package/dist/components/account/TeamMembersPage.svelte +18 -187
  16. package/dist/components/account/UserProfilePage.svelte +19 -196
  17. package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
  18. package/dist/components/content/ApiKeyList.svelte +13 -0
  19. package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
  20. package/dist/components/content/ContentPageHeader.svelte +31 -0
  21. package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
  22. package/dist/components/content/ContentPageShell.svelte +39 -0
  23. package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
  24. package/dist/components/content/DataState.svelte +44 -0
  25. package/dist/components/content/DataState.svelte.d.ts +13 -0
  26. package/dist/components/content/DescriptionList.svelte +27 -0
  27. package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
  28. package/dist/components/content/FileList.svelte +13 -0
  29. package/dist/components/content/FileList.svelte.d.ts +16 -0
  30. package/dist/components/content/FilterToolbar.svelte +24 -0
  31. package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
  32. package/dist/components/content/ImportDropzone.svelte +19 -0
  33. package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
  34. package/dist/components/content/IntegrationCard.svelte +16 -0
  35. package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
  36. package/dist/components/content/MemberList.svelte +16 -0
  37. package/dist/components/content/MemberList.svelte.d.ts +17 -0
  38. package/dist/components/content/MetricBlock.svelte +27 -0
  39. package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
  40. package/dist/components/content/NetworkTable.svelte +18 -0
  41. package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
  42. package/dist/components/content/NetworkTable.types.d.ts +4 -0
  43. package/dist/components/content/NetworkTable.types.js +1 -0
  44. package/dist/components/content/NetworkUserCard.svelte +17 -0
  45. package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
  46. package/dist/components/content/OtpInput.svelte +22 -0
  47. package/dist/components/content/OtpInput.svelte.d.ts +10 -0
  48. package/dist/components/content/PageToolbar.svelte +18 -0
  49. package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
  50. package/dist/components/content/ProjectCard.svelte +39 -0
  51. package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
  52. package/dist/components/content/SectionHeader.svelte +18 -0
  53. package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
  54. package/dist/components/content/SecurityEventTable.svelte +18 -0
  55. package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
  56. package/dist/components/content/StatusBadge.svelte +19 -0
  57. package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
  58. package/dist/components/content/SystemErrorState.svelte +18 -0
  59. package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
  60. package/dist/components/content/TeamCard.svelte +26 -0
  61. package/dist/components/content/TeamCard.svelte.d.ts +23 -0
  62. package/dist/components/content/TwoFactorStepper.svelte +11 -0
  63. package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
  64. package/dist/components/content/index.d.ts +32 -0
  65. package/dist/components/content/index.js +22 -0
  66. package/dist/components/network/TeamCrewTablePage.svelte +29 -181
  67. package/dist/components/network/UserCardsNFTPage.svelte +24 -137
  68. package/dist/components/profile/ActivityTimeline.svelte +17 -91
  69. package/dist/components/profile/ProfileCard.svelte +4 -4
  70. package/dist/components/profile/ProjectsGrid.svelte +28 -106
  71. package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
  72. package/dist/components/profile/PublicProfilePage.svelte +34 -186
  73. package/dist/components/profile/TeamsShowcase.svelte +12 -114
  74. package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
  75. package/dist/index.d.ts +36 -0
  76. package/dist/index.js +24 -0
  77. package/dist/reference-data.d.ts +171 -0
  78. package/dist/reference-data.js +47 -0
  79. package/package.json +2 -2
@@ -1,206 +1,29 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import * as Card from '../ui/card/index.js';
4
- import { Badge } from '../ui/badge/index.js';
3
+ import { Loader2 } from '@lucide/svelte';
5
4
  import { Button } from '../ui/button/index.js';
6
5
  import { Switch } from '../ui/switch/index.js';
7
- import {
8
- Briefcase, Calendar, FileText, Image, FileArchive, Link2, Loader2,
9
- Star, Award, Medal, Crown,
10
- } from '@lucide/svelte';
11
- import type { Component } from 'svelte';
12
-
6
+ import * as Card from '../ui/card/index.js';
7
+ import ContentPageShell from '../content/ContentPageShell.svelte';
8
+ import ContentPageHeader from '../content/ContentPageHeader.svelte';
9
+ import DescriptionList from '../content/DescriptionList.svelte';
10
+ import FileList from '../content/FileList.svelte';
11
+ import IntegrationCard from '../content/IntegrationCard.svelte';
12
+ import type { IntegrationSummary } from '../content/IntegrationCard.svelte';
13
+ import { referenceDemoData } from '../../reference-data.js';
13
14
  const i18n = useTranslation();
14
-
15
15
  let availableToHire = $state(true);
16
16
  let saving = $state(false);
17
-
18
- async function handleSave() {
19
- saving = true;
20
- await new Promise((r) => setTimeout(r, 800));
21
- saving = false;
22
- }
23
-
24
- const infoRows = [
25
- { label: 'profile.name', value: 'Alex Chen' },
26
- { label: 'account.phone', value: '+86 138 **** 9527' },
27
- { label: 'account.dateOfBirth', value: '1995-04-18' },
28
- { label: 'account.address', value: 'Shanghai, CN' },
29
- ];
30
-
31
- const badges: { name: string; Icon: Component; tone: string }[] = [
32
- { name: 'Top Contributor', Icon: Medal, tone: 'bg-amber-500/10 text-amber-600' },
33
- { name: 'Early Adopter', Icon: Star, tone: 'bg-blue-500/10 text-blue-600' },
34
- { name: 'Community Mentor', Icon: Award, tone: 'bg-violet-500/10 text-violet-600' },
35
- { name: 'Launch Partner', Icon: Crown, tone: 'bg-emerald-500/10 text-emerald-600' },
36
- ];
37
-
38
- interface Connection {
39
- id: string;
40
- name: string;
41
- detail: string;
42
- connected: boolean;
43
- }
44
- let connections = $state<Connection[]>([
45
- { id: 'repo', name: 'Source Control', detail: 'alexchen', connected: true },
46
- { id: 'chat', name: 'ChatOps', detail: '@alex.chen', connected: true },
47
- { id: 'drive', name: 'Cloud Drive', detail: 'alex@example.com', connected: false },
48
- ]);
49
-
50
- const files: { name: string; size: string; Icon: Component; tone: string }[] = [
51
- { name: 'portfolio-2026.pdf', size: '2.1 MB', Icon: FileText, tone: 'bg-red-500/10 text-red-600' },
52
- { name: 'avatar-pack.zip', size: '8.4 MB', Icon: FileArchive, tone: 'bg-amber-500/10 text-amber-600' },
53
- { name: 'cover-photo.png', size: '1.2 MB', Icon: Image, tone: 'bg-pink-500/10 text-pink-600' },
54
- ];
55
-
56
- const calendars = [
57
- { name: 'Work Calendar', detail: 'alex@nebulalabs.io', primary: true },
58
- { name: 'Personal Calendar', detail: 'alex@example.com', primary: false },
59
- ];
60
-
61
- function toggleConnection(id: string, connected: boolean) {
62
- connections = connections.map((c) => (c.id === id ? { ...c, connected } : c));
63
- }
17
+ async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
18
+ let integrations = $state<IntegrationSummary[]>(referenceDemoData.integrations);
19
+ const files = [{ id: '1', name: 'portfolio-2026.pdf', size: '2.1 MB', type: 'PDF' }, { id: '2', name: 'avatar-pack.zip', size: '8.4 MB', type: 'Archive' }, { id: '3', name: 'cover-photo.png', size: '1.2 MB', type: 'Image' }];
20
+ function toggle(id: string) { integrations = integrations.map((item) => item.id === id ? { ...item, connected: !item.connected } : item); }
64
21
  </script>
65
22
 
66
- <div class="space-y-6" data-svadmin-content-page="account">
67
- <div class="flex items-start justify-between gap-4">
68
- <div>
69
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.userProfile')}</h2>
70
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.userProfileDescription')}</p>
71
- </div>
72
- <Button onclick={handleSave} disabled={saving} size="sm">
73
- {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
74
- {i18n.t('common.save')}
75
- </Button>
76
- </div>
77
-
78
- <div class="grid gap-4 lg:grid-cols-3">
79
- <div class="space-y-4 lg:col-span-2">
80
- <!-- Personal Info -->
81
- <Card.Card class="border-border/60">
82
- <Card.CardHeader class="pb-3">
83
- <Card.CardTitle class="text-base">{i18n.t('account.personalInfo')}</Card.CardTitle>
84
- </Card.CardHeader>
85
- <Card.CardContent class="p-0">
86
- <div class="divide-y">
87
- {#each infoRows as row (row.label)}
88
- <div class="flex items-center justify-between px-6 py-3 text-sm">
89
- <span class="text-muted-foreground">{i18n.t(row.label)}</span>
90
- <span class="font-medium text-foreground">{row.value}</span>
91
- </div>
92
- {/each}
93
- </div>
94
- </Card.CardContent>
95
- </Card.Card>
96
-
97
- <!-- Work -->
98
- <Card.Card class="border-border/60">
99
- <Card.CardHeader class="pb-3">
100
- <Card.CardTitle class="flex items-center gap-2 text-base">
101
- <Briefcase class="h-4 w-4 text-muted-foreground" />{i18n.t('account.work')}
102
- </Card.CardTitle>
103
- </Card.CardHeader>
104
- <Card.CardContent class="space-y-4">
105
- <div class="flex items-center justify-between gap-4">
106
- <div class="space-y-1">
107
- <p class="text-sm font-medium text-foreground">{i18n.t('account.availableToHire')}</p>
108
- <p class="text-xs text-muted-foreground">{i18n.t('account.availableNow')}</p>
109
- </div>
110
- <Switch bind:checked={availableToHire} />
111
- </div>
112
- <div class="flex flex-wrap gap-1.5">
113
- {#each ['TypeScript', 'Svelte', 'Node.js', 'Design Systems'] as skill (skill)}
114
- <Badge variant="secondary" class="text-xs">{skill}</Badge>
115
- {/each}
116
- </div>
117
- </Card.CardContent>
118
- </Card.Card>
119
-
120
- <!-- Connections -->
121
- <Card.Card class="border-border/60">
122
- <Card.CardHeader class="pb-3">
123
- <Card.CardTitle class="flex items-center gap-2 text-base">
124
- <Link2 class="h-4 w-4 text-muted-foreground" />{i18n.t('account.connections')}
125
- </Card.CardTitle>
126
- </Card.CardHeader>
127
- <Card.CardContent class="p-0">
128
- <div class="divide-y">
129
- {#each connections as conn (conn.id)}
130
- <div class="flex items-center justify-between gap-4 px-6 py-3">
131
- <div class="min-w-0">
132
- <p class="text-sm font-medium text-foreground">{conn.name}</p>
133
- <p class="truncate text-xs text-muted-foreground">{conn.detail}</p>
134
- </div>
135
- <Switch checked={conn.connected} onCheckedChange={(checked) => toggleConnection(conn.id, checked)} />
136
- </div>
137
- {/each}
138
- </div>
139
- </Card.CardContent>
140
- </Card.Card>
141
- </div>
142
-
143
- <div class="space-y-4">
144
- <!-- Badges -->
145
- <Card.Card class="border-border/60">
146
- <Card.CardHeader class="pb-3">
147
- <Card.CardTitle class="text-base">{i18n.t('account.badges')}</Card.CardTitle>
148
- </Card.CardHeader>
149
- <Card.CardContent>
150
- <div class="grid grid-cols-2 gap-2">
151
- {#each badges as badge (badge.name)}
152
- <div class="flex items-center gap-2 rounded-lg border border-border/60 p-2.5">
153
- <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg {badge.tone}">
154
- <badge.Icon class="h-4 w-4" />
155
- </div>
156
- <span class="text-xs font-medium text-foreground">{badge.name}</span>
157
- </div>
158
- {/each}
159
- </div>
160
- </Card.CardContent>
161
- </Card.Card>
162
-
163
- <!-- Calendar Accounts -->
164
- <Card.Card class="border-border/60">
165
- <Card.CardHeader class="pb-3">
166
- <Card.CardTitle class="flex items-center gap-2 text-base">
167
- <Calendar class="h-4 w-4 text-muted-foreground" />{i18n.t('account.calendarAccounts')}
168
- </Card.CardTitle>
169
- </Card.CardHeader>
170
- <Card.CardContent class="space-y-3">
171
- {#each calendars as cal (cal.name)}
172
- <div class="flex items-center justify-between gap-2">
173
- <div class="min-w-0">
174
- <p class="truncate text-sm font-medium text-foreground">{cal.name}</p>
175
- <p class="truncate text-xs text-muted-foreground">{cal.detail}</p>
176
- </div>
177
- {#if cal.primary}
178
- <Badge variant="secondary" class="shrink-0 text-[10px]">{i18n.t('account.active')}</Badge>
179
- {/if}
180
- </div>
181
- {/each}
182
- </Card.CardContent>
183
- </Card.Card>
184
-
185
- <!-- My Files -->
186
- <Card.Card class="border-border/60">
187
- <Card.CardHeader class="pb-3">
188
- <Card.CardTitle class="text-base">{i18n.t('account.myFiles')}</Card.CardTitle>
189
- </Card.CardHeader>
190
- <Card.CardContent class="space-y-2.5">
191
- {#each files as file (file.name)}
192
- <div class="flex items-center gap-3">
193
- <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg {file.tone}">
194
- <file.Icon class="h-4 w-4" />
195
- </div>
196
- <div class="min-w-0 flex-1">
197
- <p class="truncate text-sm font-medium text-foreground">{file.name}</p>
198
- <p class="text-xs text-muted-foreground">{file.size}</p>
199
- </div>
200
- </div>
201
- {/each}
202
- </Card.CardContent>
203
- </Card.Card>
204
- </div>
23
+ <ContentPageShell pageId="account-user-profile" width="wide">
24
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t('account.userProfile')} description={i18n.t('account.userProfileDescription')} /><Button onclick={save} disabled={saving} size="sm">{#if saving}<Loader2 class="size-3.5 animate-spin" />{/if}{i18n.t('common.save')}</Button></div>
25
+ <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
26
+ <div class="space-y-4"><Card.Card><Card.CardContent class="space-y-5 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.personalInfo')}</h2><p class="mt-1 text-sm text-muted-foreground">Keep your contact and working details current.</p></div><DescriptionList columns={2} items={[{ label: i18n.t('profile.name'), value: 'Alex Chen' }, { label: i18n.t('account.phone'), value: '+86 138 **** 9527' }, { label: i18n.t('account.dateOfBirth'), value: '1995-04-18' }, { label: i18n.t('account.address'), value: 'Shanghai, CN' }]} /></Card.CardContent></Card.Card><Card.Card><Card.CardContent class="space-y-4 p-5"><div class="flex items-center justify-between gap-3"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.work')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.availableNow')}</p></div><Switch bind:checked={availableToHire} /></div><div class="flex flex-wrap gap-1.5">{#each ['TypeScript', 'Svelte', 'Node.js', 'Design Systems'] as skill (skill)}<span class="rounded-md border border-border px-2 py-1 text-xs text-muted-foreground">{skill}</span>{/each}</div></Card.CardContent></Card.Card><section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('account.connections')}</h2>{#each integrations as integration (integration.id)}<IntegrationCard {integration} onconnect={() => toggle(integration.id)} />{/each}</section></div>
27
+ <div class="space-y-4"><Card.Card><Card.CardContent class="space-y-3 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.myFiles')}</h2><FileList files={files} /></Card.CardContent></Card.Card><Card.Card><Card.CardContent class="space-y-3 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.calendarAccounts')}</h2><DescriptionList items={[{ label: 'Work Calendar', value: 'alex@nebulalabs.io' }, { label: 'Personal Calendar', value: 'alex@example.com' }]} /></Card.CardContent></Card.Card></div>
205
28
  </div>
206
- </div>
29
+ </ContentPageShell>
@@ -1,4 +1,3 @@
1
- import type { Component } from 'svelte';
2
- declare const UserProfilePage: Component<Record<string, never>, {}, "">;
1
+ declare const UserProfilePage: import("svelte").Component<Record<string, never>, {}, "">;
3
2
  type UserProfilePage = ReturnType<typeof UserProfilePage>;
4
3
  export default UserProfilePage;
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { Copy, KeyRound, Trash2 } from '@lucide/svelte';
3
+ import { Button } from '../ui/button/index.js';
4
+ export interface ApiKeySummary { id: string; name: string; prefix: string; createdAt?: string; lastUsedAt?: string; }
5
+ interface Props { keys?: ApiKeySummary[]; oncopy?: (key: ApiKeySummary) => void; onrevoke?: (key: ApiKeySummary) => void; class?: string; }
6
+ let { keys = [], oncopy, onrevoke, class: className = '' }: Props = $props();
7
+ </script>
8
+ <div class={'divide-y divide-border overflow-hidden rounded-lg border border-border bg-card ' + className}>
9
+ {#each keys as apiKey (apiKey.id)}
10
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3"><span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"><KeyRound class="size-4" /></span><div class="min-w-0 flex-1"><p class="truncate text-sm font-medium text-foreground">{apiKey.name}</p><p class="font-mono text-xs text-muted-foreground">{apiKey.prefix}********</p></div><div class="text-right text-xs text-muted-foreground"><p>Created {apiKey.createdAt ?? 'Recently'}</p><p>Used {apiKey.lastUsedAt ?? 'Never'}</p></div><div class="flex items-center gap-1">{#if oncopy}<Button variant="ghost" size="icon-sm" aria-label={'Copy ' + apiKey.name} onclick={() => oncopy?.(apiKey)}><Copy class="size-4" /></Button>{/if}{#if onrevoke}<Button variant="ghost" size="icon-sm" aria-label={'Revoke ' + apiKey.name} onclick={() => onrevoke?.(apiKey)}><Trash2 class="size-4" /></Button>{/if}</div></div>
11
+ {/each}
12
+ {#if keys.length === 0}<div class="p-6 text-center text-sm text-muted-foreground">No API keys</div>{/if}
13
+ </div>
@@ -0,0 +1,16 @@
1
+ export interface ApiKeySummary {
2
+ id: string;
3
+ name: string;
4
+ prefix: string;
5
+ createdAt?: string;
6
+ lastUsedAt?: string;
7
+ }
8
+ interface Props {
9
+ keys?: ApiKeySummary[];
10
+ oncopy?: (key: ApiKeySummary) => void;
11
+ onrevoke?: (key: ApiKeySummary) => void;
12
+ class?: string;
13
+ }
14
+ declare const ApiKeyList: import("svelte").Component<Props, {}, "">;
15
+ type ApiKeyList = ReturnType<typeof ApiKeyList>;
16
+ export default ApiKeyList;
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ interface Props {
4
+ title: string;
5
+ eyebrow?: string;
6
+ description?: string;
7
+ breadcrumbs?: string[];
8
+ actions?: Snippet;
9
+ class?: string;
10
+ }
11
+ let { title, eyebrow, description, breadcrumbs = [], actions, class: className = '' }: Props = $props();
12
+ </script>
13
+
14
+ <header class={'space-y-3 ' + className} data-svadmin-page-header>
15
+ {#if breadcrumbs.length > 0}
16
+ <nav aria-label="Breadcrumb" class="flex items-center gap-2 text-xs text-muted-foreground">
17
+ {#each breadcrumbs as crumb, index (crumb)}
18
+ {#if index > 0}<span aria-hidden="true">/</span>{/if}
19
+ <span class={index === breadcrumbs.length - 1 ? 'text-foreground' : ''}>{crumb}</span>
20
+ {/each}
21
+ </nav>
22
+ {/if}
23
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
24
+ <div class="min-w-0 space-y-1">
25
+ {#if eyebrow}<p class="text-xs font-medium text-muted-foreground">{eyebrow}</p>{/if}
26
+ <h1 class="text-xl font-semibold leading-tight tracking-normal text-foreground">{title}</h1>
27
+ {#if description}<p class="max-w-2xl text-sm leading-6 text-muted-foreground">{description}</p>{/if}
28
+ </div>
29
+ {#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
30
+ </div>
31
+ </header>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title: string;
4
+ eyebrow?: string;
5
+ description?: string;
6
+ breadcrumbs?: string[];
7
+ actions?: Snippet;
8
+ class?: string;
9
+ }
10
+ declare const ContentPageHeader: import("svelte").Component<Props, {}, "">;
11
+ type ContentPageHeader = ReturnType<typeof ContentPageHeader>;
12
+ export default ContentPageHeader;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import ContentPageHeader from './ContentPageHeader.svelte';
4
+
5
+ interface Props {
6
+ title?: string;
7
+ eyebrow?: string;
8
+ description?: string;
9
+ actions?: Snippet;
10
+ children: Snippet;
11
+ pageId?: string;
12
+ width?: 'narrow' | 'default' | 'wide';
13
+ class?: string;
14
+ }
15
+
16
+ let {
17
+ title,
18
+ eyebrow,
19
+ description,
20
+ actions,
21
+ children,
22
+ pageId = 'content',
23
+ width = 'default',
24
+ class: className = '',
25
+ }: Props = $props();
26
+
27
+ const widthClass = $derived(
28
+ width === 'narrow'
29
+ ? 'max-w-3xl'
30
+ : width === 'wide'
31
+ ? 'max-w-[92rem]'
32
+ : 'max-w-[74rem]',
33
+ );
34
+ </script>
35
+
36
+ <div data-svadmin-content-page={pageId} class={'mx-auto w-full ' + widthClass + ' space-y-6 ' + className}>
37
+ {#if title}<ContentPageHeader {title} {eyebrow} {description} {actions} />{/if}
38
+ {@render children()}
39
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title?: string;
4
+ eyebrow?: string;
5
+ description?: string;
6
+ actions?: Snippet;
7
+ children: Snippet;
8
+ pageId?: string;
9
+ width?: 'narrow' | 'default' | 'wide';
10
+ class?: string;
11
+ }
12
+ declare const ContentPageShell: import("svelte").Component<Props, {}, "">;
13
+ type ContentPageShell = ReturnType<typeof ContentPageShell>;
14
+ export default ContentPageShell;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { AlertTriangle, Inbox, LockKeyhole, RefreshCw } from '@lucide/svelte';
4
+ import { Button } from '../ui/button/index.js';
5
+ import * as Alert from '../ui/alert/index.js';
6
+ import { Skeleton } from '../ui/skeleton/index.js';
7
+ export type DataStateKind = 'loading' | 'empty' | 'error' | 'forbidden';
8
+ interface Props {
9
+ state: DataStateKind;
10
+ title?: string;
11
+ description?: string;
12
+ retry?: () => void;
13
+ action?: Snippet;
14
+ class?: string;
15
+ }
16
+ let { state, title, description, retry, action, class: className = '' }: Props = $props();
17
+ const defaults = {
18
+ loading: ['Loading', 'Fetching the latest data.'],
19
+ empty: ['Nothing here yet', 'New records will appear here when they are available.'],
20
+ error: ['Unable to load data', 'Try again or check the connection before continuing.'],
21
+ forbidden: ['Access restricted', 'You do not have permission to view this content.'],
22
+ } as const;
23
+ const resolvedTitle = $derived(title ?? defaults[state][0]);
24
+ const resolvedDescription = $derived(description ?? defaults[state][1]);
25
+ </script>
26
+
27
+ {#if state === 'loading'}
28
+ <div class={'space-y-3 rounded-lg border border-border bg-card p-5 ' + className} aria-busy="true">
29
+ <Skeleton class="h-5 w-40" /><Skeleton class="h-4 w-full max-w-md" /><Skeleton class="h-20 w-full" />
30
+ </div>
31
+ {:else if state === 'error' || state === 'forbidden'}
32
+ <Alert.Root variant={state === 'error' ? 'destructive' : 'warning'} class={className}>
33
+ {#if state === 'error'}<AlertTriangle class="size-4" />{:else}<LockKeyhole class="size-4" />{/if}
34
+ <Alert.Title>{resolvedTitle}</Alert.Title>
35
+ <Alert.Description>{resolvedDescription}</Alert.Description>
36
+ {#if retry}<Button variant="outline" size="sm" class="mt-3" onclick={retry}><RefreshCw class="size-3.5" />Retry</Button>{/if}
37
+ {#if action}<div class="mt-3">{@render action()}</div>{/if}
38
+ </Alert.Root>
39
+ {:else}
40
+ <div class={'flex min-h-40 flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card p-6 text-center ' + className}>
41
+ <Inbox class="size-8 text-muted-foreground" /><h3 class="mt-3 text-sm font-semibold text-foreground">{resolvedTitle}</h3><p class="mt-1 max-w-sm text-sm text-muted-foreground">{resolvedDescription}</p>
42
+ {#if action}<div class="mt-4">{@render action()}</div>{/if}
43
+ </div>
44
+ {/if}
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type DataStateKind = 'loading' | 'empty' | 'error' | 'forbidden';
3
+ interface Props {
4
+ state: DataStateKind;
5
+ title?: string;
6
+ description?: string;
7
+ retry?: () => void;
8
+ action?: Snippet;
9
+ class?: string;
10
+ }
11
+ declare const DataState: import("svelte").Component<Props, {}, "">;
12
+ type DataState = ReturnType<typeof DataState>;
13
+ export default DataState;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ export interface DescriptionItem {
3
+ label: string;
4
+ value: string | number;
5
+ description?: string;
6
+ href?: string;
7
+ }
8
+ interface Props {
9
+ items?: DescriptionItem[];
10
+ columns?: 1 | 2;
11
+ class?: string;
12
+ }
13
+ let { items = [], columns = 1, class: className = '' }: Props = $props();
14
+ const columnClass = $derived(columns === 2 ? 'sm:grid-cols-2' : 'grid-cols-1');
15
+ </script>
16
+
17
+ <dl class={'grid gap-x-6 gap-y-5 ' + columnClass + ' ' + className}>
18
+ {#each items as item (item.label)}
19
+ <div class="min-w-0">
20
+ <dt class="text-xs font-medium text-muted-foreground">{item.label}</dt>
21
+ <dd class="mt-1 break-words text-sm font-medium text-foreground">
22
+ {#if item.href}<a class="text-primary underline-offset-4 hover:underline" href={item.href}>{item.value}</a>{:else}{item.value}{/if}
23
+ </dd>
24
+ {#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}
25
+ </div>
26
+ {/each}
27
+ </dl>
@@ -0,0 +1,14 @@
1
+ export interface DescriptionItem {
2
+ label: string;
3
+ value: string | number;
4
+ description?: string;
5
+ href?: string;
6
+ }
7
+ interface Props {
8
+ items?: DescriptionItem[];
9
+ columns?: 1 | 2;
10
+ class?: string;
11
+ }
12
+ declare const DescriptionList: import("svelte").Component<Props, {}, "">;
13
+ type DescriptionList = ReturnType<typeof DescriptionList>;
14
+ export default DescriptionList;
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { Download, File, Trash2 } from '@lucide/svelte';
3
+ import { Button } from '../ui/button/index.js';
4
+ export interface FileItem { id?: string; name: string; size?: string; type?: string; updatedAt?: string; }
5
+ interface Props { files?: FileItem[]; ondownload?: (file: FileItem) => void; onremove?: (file: FileItem) => void; class?: string; }
6
+ let { files = [], ondownload, onremove, class: className = '' }: Props = $props();
7
+ </script>
8
+ <div class={'divide-y divide-border overflow-hidden rounded-lg border border-border bg-card ' + className}>
9
+ {#each files as file, index (file.id ?? file.name + '-' + index)}
10
+ <div class="flex items-center gap-3 px-4 py-3"><span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"><File class="size-4" /></span><div class="min-w-0 flex-1"><p class="truncate text-sm font-medium text-foreground">{file.name}</p><p class="text-xs text-muted-foreground">{file.size ?? file.type ?? 'File'}{file.updatedAt ? ' / ' + file.updatedAt : ''}</p></div>{#if ondownload}<Button variant="ghost" size="icon-sm" aria-label={'Download ' + file.name} onclick={() => ondownload?.(file)}><Download class="size-4" /></Button>{/if}{#if onremove}<Button variant="ghost" size="icon-sm" aria-label={'Remove ' + file.name} onclick={() => onremove?.(file)}><Trash2 class="size-4" /></Button>{/if}</div>
11
+ {/each}
12
+ {#if files.length === 0}<div class="p-6 text-center text-sm text-muted-foreground">No files</div>{/if}
13
+ </div>
@@ -0,0 +1,16 @@
1
+ export interface FileItem {
2
+ id?: string;
3
+ name: string;
4
+ size?: string;
5
+ type?: string;
6
+ updatedAt?: string;
7
+ }
8
+ interface Props {
9
+ files?: FileItem[];
10
+ ondownload?: (file: FileItem) => void;
11
+ onremove?: (file: FileItem) => void;
12
+ class?: string;
13
+ }
14
+ declare const FileList: import("svelte").Component<Props, {}, "">;
15
+ type FileList = ReturnType<typeof FileList>;
16
+ export default FileList;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { Search, X } from '@lucide/svelte';
4
+ import { Button } from '../ui/button/index.js';
5
+ import { Input } from '../ui/input/index.js';
6
+ interface Props {
7
+ query?: string;
8
+ placeholder?: string;
9
+ filters?: Snippet;
10
+ actions?: Snippet;
11
+ class?: string;
12
+ }
13
+ let { query = $bindable(''), placeholder = 'Search', filters, actions, class: className = '' }: Props = $props();
14
+ </script>
15
+
16
+ <div class={'flex flex-col gap-2 sm:flex-row sm:items-center ' + className}>
17
+ <div class="relative min-w-0 flex-1">
18
+ <Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
19
+ <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}
21
+ </div>
22
+ {#if filters}<div class="flex flex-wrap items-center gap-2">{@render filters()}</div>{/if}
23
+ {#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
24
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ query?: string;
4
+ placeholder?: string;
5
+ filters?: Snippet;
6
+ actions?: Snippet;
7
+ class?: string;
8
+ }
9
+ declare const FilterToolbar: import("svelte").Component<Props, {}, "query">;
10
+ type FilterToolbar = ReturnType<typeof FilterToolbar>;
11
+ export default FilterToolbar;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { UploadCloud } from '@lucide/svelte';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Input } from '../ui/input/index.js';
5
+ interface Props { accept?: string; loading?: boolean; status?: string; onimport?: (files: FileList | null) => void; class?: string; }
6
+ let { accept = '.csv,.xlsx', loading = false, status = '', onimport, class: className = '' }: Props = $props();
7
+ let files = $state<FileList | null>(null);
8
+ function choose(event: Event) {
9
+ files = (event.currentTarget as HTMLInputElement).files;
10
+ if (files?.length) onimport?.(files);
11
+ }
12
+ </script>
13
+ <div class={'rounded-lg border border-dashed border-border bg-card p-6 text-center ' + className} data-svadmin-import-dropzone>
14
+ <UploadCloud class="mx-auto size-8 text-muted-foreground" /><p class="mt-3 text-sm font-medium text-foreground">Import records</p><p class="mt-1 text-xs text-muted-foreground">CSV or spreadsheet files are accepted.</p>
15
+ <Input class="mx-auto mt-4 max-w-sm" type="file" {accept} onchange={choose} disabled={loading} />
16
+ {#if loading}<p class="mt-3 text-xs text-muted-foreground" aria-live="polite">Importing...</p>{/if}
17
+ {#if status}<p class="mt-3 text-xs text-success" role="status">{status}</p>{/if}
18
+ {#if !loading && files?.length}<Button class="mt-4" size="sm" onclick={() => onimport?.(files)}>Import {files.length} file{files.length === 1 ? '' : 's'}</Button>{/if}
19
+ </div>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ accept?: string;
3
+ loading?: boolean;
4
+ status?: string;
5
+ onimport?: (files: FileList | null) => void;
6
+ class?: string;
7
+ }
8
+ declare const ImportDropzone: import("svelte").Component<Props, {}, "">;
9
+ type ImportDropzone = ReturnType<typeof ImportDropzone>;
10
+ export default ImportDropzone;
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import { Check, ExternalLink, PlugZap } from '@lucide/svelte';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Badge } from '../ui/badge/index.js';
5
+ import * as Card from '../ui/card/index.js';
6
+ export interface IntegrationSummary { id: string; name: string; description?: string; account?: string; connected?: boolean; }
7
+ interface Props { integration: IntegrationSummary; onconnect?: () => void; class?: string; }
8
+ let { integration, onconnect, class: className = '' }: Props = $props();
9
+ </script>
10
+ <Card.Card class={className}>
11
+ <Card.CardContent class="flex items-start gap-3 p-4">
12
+ <span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"><PlugZap class="size-4" /></span>
13
+ <div class="min-w-0 flex-1"><div class="flex flex-wrap items-center gap-2"><h3 class="text-sm font-semibold text-foreground">{integration.name}</h3>{#if integration.connected}<Badge variant="outline" class="border-success/30 bg-success/10 text-success"><Check class="size-3" />Connected</Badge>{/if}</div><p class="mt-1 text-sm text-muted-foreground">{integration.description ?? 'Connect this provider to unlock synchronized workflows.'}</p>{#if integration.account}<p class="mt-2 text-xs text-muted-foreground">{integration.account}</p>{/if}</div>
14
+ {#if onconnect}<Button variant={integration.connected ? 'outline' : 'default'} size="sm" onclick={onconnect}>{integration.connected ? 'Manage' : 'Connect'}{#if !integration.connected}<ExternalLink class="size-3.5" />{/if}</Button>{/if}
15
+ </Card.CardContent>
16
+ </Card.Card>
@@ -0,0 +1,15 @@
1
+ export interface IntegrationSummary {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ account?: string;
6
+ connected?: boolean;
7
+ }
8
+ interface Props {
9
+ integration: IntegrationSummary;
10
+ onconnect?: () => void;
11
+ class?: string;
12
+ }
13
+ declare const IntegrationCard: import("svelte").Component<Props, {}, "">;
14
+ type IntegrationCard = ReturnType<typeof IntegrationCard>;
15
+ export default IntegrationCard;
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import { MoreHorizontal, UserRound } from '@lucide/svelte';
3
+ import { Avatar } from '../ui/avatar/index.js';
4
+ import { Button } from '../ui/button/index.js';
5
+ import StatusBadge from './StatusBadge.svelte';
6
+ 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();
9
+ const initials = (name: string) => name.split(' ').map((part) => part[0]).join('').slice(0, 2).toUpperCase();
10
+ </script>
11
+ <div class={'divide-y divide-border overflow-hidden rounded-lg border border-border bg-card ' + className}>
12
+ {#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>
14
+ {/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
+ </div>