@svadmin/ui 0.43.0 → 0.45.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/ForgotPasswordPage.svelte +4 -8
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/ProfilePage.svelte +4 -23
- package/dist/components/ResourceOperationsPage.svelte +84 -223
- package/dist/components/ResourceOperationsPage.svelte.d.ts +1 -1
- package/dist/components/SecuritySettings.svelte +4 -10
- package/dist/components/Toast.svelte +7 -5
- package/dist/components/account/GetStartedPage.svelte +50 -29
- package/dist/components/account/MembersStarterPage.svelte +49 -92
- package/dist/components/content/FeedbackNotice.svelte +61 -0
- package/dist/components/content/FeedbackNotice.svelte.d.ts +13 -0
- package/dist/components/content/index.d.ts +2 -0
- package/dist/components/content/index.js +1 -0
- package/dist/components/profile/PublicProfilePage.svelte +13 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/reference-data.js +8 -0
- package/package.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Label } from './ui/label/index.js';
|
|
8
8
|
import * as Alert from './ui/alert/index.js';
|
|
9
9
|
import SvadminLogo from './SvadminLogo.svelte';
|
|
10
|
-
import { User, ArrowLeft,
|
|
10
|
+
import { User, ArrowLeft, Loader2, AlertCircle } from '@lucide/svelte';
|
|
11
11
|
|
|
12
12
|
const i18n = useTranslation();
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
}>();
|
|
17
17
|
|
|
18
18
|
const adminContext = captureAdminContext();
|
|
19
|
-
const forgot = useForgotPassword();
|
|
19
|
+
const forgot = useForgotPassword({ successNotification: false });
|
|
20
20
|
|
|
21
21
|
let identifier = $state('');
|
|
22
22
|
let error = $state('');
|
|
@@ -82,14 +82,14 @@
|
|
|
82
82
|
<div class="space-y-1 mb-8">
|
|
83
83
|
<h1 class="text-2xl font-semibold tracking-tight">
|
|
84
84
|
{#if sent}
|
|
85
|
-
{i18n.t('auth.
|
|
85
|
+
{i18n.t('auth.resetLinkSentTitle')}
|
|
86
86
|
{:else}
|
|
87
87
|
{i18n.t('auth.forgotPassword')}
|
|
88
88
|
{/if}
|
|
89
89
|
</h1>
|
|
90
90
|
<p class="text-sm text-muted-foreground">
|
|
91
91
|
{#if sent}
|
|
92
|
-
{i18n.t('auth.
|
|
92
|
+
{i18n.t('auth.resetLinkSentDescription')}
|
|
93
93
|
{:else}
|
|
94
94
|
{i18n.t('auth.forgotPasswordDescription')}
|
|
95
95
|
{/if}
|
|
@@ -98,10 +98,6 @@
|
|
|
98
98
|
|
|
99
99
|
{#if sent}
|
|
100
100
|
<div class="space-y-4">
|
|
101
|
-
<div class="flex items-center gap-3 rounded-lg border bg-muted/50 p-4">
|
|
102
|
-
<CheckCircle class="h-5 w-5 text-primary shrink-0" />
|
|
103
|
-
<p class="text-sm text-muted-foreground">{i18n.t('auth.resetLinkSent')}</p>
|
|
104
|
-
</div>
|
|
105
101
|
<Button variant="outline" class="w-full" onclick={() => adminContext.navigate('/login')}>
|
|
106
102
|
<ArrowLeft class="h-4 w-4 mr-2" />
|
|
107
103
|
{i18n.t('auth.backToLogin')}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}>();
|
|
36
36
|
|
|
37
37
|
const adminContext = captureAdminContext();
|
|
38
|
-
const login = useLogin({ errorMessage: false });
|
|
38
|
+
const login = useLogin({ successNotification: false, errorMessage: false });
|
|
39
39
|
const authProvider = $derived(adminContext.authProvider);
|
|
40
40
|
|
|
41
41
|
function getInitialIdentifier() {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
captureAdminContext,
|
|
4
4
|
useGetIdentity,
|
|
5
|
+
useNotification,
|
|
5
6
|
useUpdatePassword,
|
|
6
7
|
type AuthProvider,
|
|
7
8
|
} from '@svadmin/core';
|
|
@@ -12,12 +13,13 @@
|
|
|
12
13
|
import * as Card from './ui/card/index.js';
|
|
13
14
|
import * as Alert from './ui/alert/index.js';
|
|
14
15
|
import PasswordInput from './PasswordInput.svelte';
|
|
15
|
-
import { User, Mail, Lock, Loader2, AlertCircle,
|
|
16
|
+
import { User, Mail, Lock, Loader2, AlertCircle, Camera } from '@lucide/svelte';
|
|
16
17
|
|
|
17
18
|
const i18n = useTranslation();
|
|
18
19
|
const adminContext = captureAdminContext();
|
|
19
20
|
|
|
20
21
|
const identity = useGetIdentity();
|
|
22
|
+
const notification = useNotification();
|
|
21
23
|
const updatePw = useUpdatePassword();
|
|
22
24
|
|
|
23
25
|
const authProvider = $derived(adminContext.authProvider);
|
|
@@ -27,7 +29,6 @@
|
|
|
27
29
|
let editingProfile = $state(false);
|
|
28
30
|
let editName = $state('');
|
|
29
31
|
let profileSaving = $state(false);
|
|
30
|
-
let profileSuccess = $state(false);
|
|
31
32
|
let profileError = $state('');
|
|
32
33
|
let avatarUploading = $state(false);
|
|
33
34
|
let fileInput = $state<HTMLInputElement>();
|
|
@@ -35,21 +36,18 @@
|
|
|
35
36
|
let newPassword = $state('');
|
|
36
37
|
let confirmPassword = $state('');
|
|
37
38
|
let pwError = $state('');
|
|
38
|
-
let pwSuccess = $state(false);
|
|
39
39
|
let scopeEpoch = 0;
|
|
40
40
|
|
|
41
41
|
function resetProfileScope(): void {
|
|
42
42
|
editingProfile = false;
|
|
43
43
|
editName = '';
|
|
44
44
|
profileSaving = false;
|
|
45
|
-
profileSuccess = false;
|
|
46
45
|
profileError = '';
|
|
47
46
|
avatarUploading = false;
|
|
48
47
|
currentPassword = '';
|
|
49
48
|
newPassword = '';
|
|
50
49
|
confirmPassword = '';
|
|
51
50
|
pwError = '';
|
|
52
|
-
pwSuccess = false;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
function isActiveScope(
|
|
@@ -75,7 +73,6 @@
|
|
|
75
73
|
function startEditProfile() {
|
|
76
74
|
editName = identity.data?.name ?? '';
|
|
77
75
|
editingProfile = true;
|
|
78
|
-
profileSuccess = false;
|
|
79
76
|
profileError = '';
|
|
80
77
|
}
|
|
81
78
|
|
|
@@ -90,9 +87,9 @@
|
|
|
90
87
|
const result = await provider.updateProfile({ name: editName });
|
|
91
88
|
if (!isActiveScope(epoch, provider, scopedTenantIdentity)) return;
|
|
92
89
|
if (result.success) {
|
|
93
|
-
profileSuccess = true;
|
|
94
90
|
editingProfile = false;
|
|
95
91
|
identity.refetch();
|
|
92
|
+
notification.success(i18n.t('common.updateSuccess'), 3000);
|
|
96
93
|
} else {
|
|
97
94
|
profileError = result.error?.message ?? i18n.t('common.operationFailed');
|
|
98
95
|
}
|
|
@@ -123,7 +120,6 @@
|
|
|
123
120
|
async function handlePasswordChange(e: SubmitEvent) {
|
|
124
121
|
e.preventDefault();
|
|
125
122
|
pwError = '';
|
|
126
|
-
pwSuccess = false;
|
|
127
123
|
|
|
128
124
|
if (!newPassword) { pwError = i18n.t('auth.passwordRequired'); return; }
|
|
129
125
|
if (newPassword !== confirmPassword) { pwError = i18n.t('auth.passwordMismatch'); return; }
|
|
@@ -138,7 +134,6 @@
|
|
|
138
134
|
});
|
|
139
135
|
if (!isActiveScope(epoch, provider, scopedTenantIdentity)) return;
|
|
140
136
|
if (result.success) {
|
|
141
|
-
pwSuccess = true;
|
|
142
137
|
currentPassword = '';
|
|
143
138
|
newPassword = '';
|
|
144
139
|
confirmPassword = '';
|
|
@@ -174,13 +169,6 @@
|
|
|
174
169
|
{i18n.t('common.loading')}
|
|
175
170
|
</div>
|
|
176
171
|
{:else if identity.data}
|
|
177
|
-
{#if profileSuccess}
|
|
178
|
-
<Alert.Root class="border-success/30 bg-success/5 text-success mb-4">
|
|
179
|
-
<CheckCircle class="h-4 w-4" />
|
|
180
|
-
<Alert.Description>{i18n.t('common.updateSuccess')}</Alert.Description>
|
|
181
|
-
</Alert.Root>
|
|
182
|
-
{/if}
|
|
183
|
-
|
|
184
172
|
<div class="flex items-start gap-6">
|
|
185
173
|
<!-- Avatar -->
|
|
186
174
|
<div class="shrink-0 relative group">
|
|
@@ -292,13 +280,6 @@
|
|
|
292
280
|
</Alert.Root>
|
|
293
281
|
{/if}
|
|
294
282
|
|
|
295
|
-
{#if pwSuccess}
|
|
296
|
-
<Alert.Root class="border-success/30 bg-success/5 text-success">
|
|
297
|
-
<CheckCircle class="h-4 w-4" />
|
|
298
|
-
<Alert.Description>{i18n.t('profile.passwordChanged')}</Alert.Description>
|
|
299
|
-
</Alert.Root>
|
|
300
|
-
{/if}
|
|
301
|
-
|
|
302
283
|
<PasswordInput
|
|
303
284
|
id="current-password"
|
|
304
285
|
label={i18n.t('profile.currentPassword')}
|
|
@@ -28,13 +28,19 @@
|
|
|
28
28
|
type WorkspaceStyle =
|
|
29
29
|
| 'inventory'
|
|
30
30
|
| 'operations'
|
|
31
|
+
| 'orders'
|
|
31
32
|
| 'people'
|
|
32
33
|
| 'calendar'
|
|
33
34
|
| 'communications'
|
|
34
35
|
| 'crm'
|
|
35
36
|
| 'property'
|
|
36
37
|
| 'ai'
|
|
37
|
-
| 'store'
|
|
38
|
+
| 'store'
|
|
39
|
+
| 'planning'
|
|
40
|
+
| 'generation'
|
|
41
|
+
| 'billing'
|
|
42
|
+
| 'security'
|
|
43
|
+
| 'referral';
|
|
38
44
|
|
|
39
45
|
interface Props {
|
|
40
46
|
resourceName: string;
|
|
@@ -72,228 +78,83 @@
|
|
|
72
78
|
const adminContext = captureAdminContext();
|
|
73
79
|
</script>
|
|
74
80
|
|
|
75
|
-
<div class="space-y-6" data-svadmin-resource-operations={resourceName} data-svadmin-workspace-style={workspaceStyle}>
|
|
76
|
-
<
|
|
77
|
-
<div class="min-w-0
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{#if Icon}
|
|
81
|
-
<span class="flex h-10 w-10 shrink-0 items-center justify-center text-primary">
|
|
82
|
-
<Icon class="h-6 w-6" />
|
|
83
|
-
</span>
|
|
84
|
-
{/if}
|
|
85
|
-
<div class="min-w-0">
|
|
86
|
-
<h1 class="text-3xl font-semibold tracking-tight">{title}</h1>
|
|
87
|
-
<p class="mt-2 max-w-2xl text-sm text-muted-foreground">{description}</p>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
<Button class="mt-6" onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
|
|
91
|
-
{actionLabel}
|
|
92
|
-
</Button>
|
|
81
|
+
<div class="space-y-6" data-svadmin-resource-operations={resourceName} data-svadmin-workspace-style={workspaceStyle} data-svadmin-layout-identity={workspaceStyle}>
|
|
82
|
+
<header class="flex flex-col gap-4 border-b border-border pb-5 sm:flex-row sm:items-start sm:justify-between" data-svadmin-ops-hero>
|
|
83
|
+
<div class="flex min-w-0 items-start gap-3">
|
|
84
|
+
{#if Icon}<span class="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-card text-primary"><Icon class="size-4" /></span>{/if}
|
|
85
|
+
<div class="min-w-0"><Badge variant="outline">{eyebrow}</Badge><h1 class="mt-2 text-xl font-semibold text-foreground">{title}</h1><p class="mt-1 max-w-3xl text-sm leading-6 text-muted-foreground">{description}</p></div>
|
|
93
86
|
</div>
|
|
87
|
+
<Button class="shrink-0" size="sm" onclick={() => adminContext.navigate(`/${resourceName}/create`)}>{actionLabel}</Button>
|
|
88
|
+
</header>
|
|
94
89
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</Card.Root>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</div>
|
|
166
|
-
{#if item.description}
|
|
167
|
-
<p class="mt-2 text-sm text-muted-foreground">{item.description}</p>
|
|
168
|
-
{/if}
|
|
169
|
-
{#if item.meta}
|
|
170
|
-
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
171
|
-
{/if}
|
|
172
|
-
</div>
|
|
173
|
-
{/each}
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
{:else if workspaceStyle === 'communications'}
|
|
177
|
-
<div class="grid gap-4 lg:grid-cols-[0.82fr_1.18fr]">
|
|
178
|
-
<div class="space-y-2">
|
|
179
|
-
{#each lanes as lane (lane.label)}
|
|
180
|
-
<div class="flex items-center justify-between rounded-lg border bg-muted/25 px-3 py-2">
|
|
181
|
-
<div>
|
|
182
|
-
<p class="text-sm font-medium">{lane.label}</p>
|
|
183
|
-
{#if lane.hint}
|
|
184
|
-
<p class="text-xs text-muted-foreground">{lane.hint}</p>
|
|
185
|
-
{/if}
|
|
186
|
-
</div>
|
|
187
|
-
<Badge variant="outline">{lane.value}</Badge>
|
|
188
|
-
</div>
|
|
189
|
-
{/each}
|
|
190
|
-
</div>
|
|
191
|
-
<div class="divide-y rounded-lg border">
|
|
192
|
-
{#each highlights as item (item.title)}
|
|
193
|
-
<div class="px-4 py-3">
|
|
194
|
-
<div class="flex items-center justify-between gap-3">
|
|
195
|
-
<p class="font-medium">{item.title}</p>
|
|
196
|
-
{#if item.badge}
|
|
197
|
-
<Badge>{item.badge}</Badge>
|
|
198
|
-
{/if}
|
|
199
|
-
</div>
|
|
200
|
-
{#if item.description}
|
|
201
|
-
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
202
|
-
{/if}
|
|
203
|
-
{#if item.meta}
|
|
204
|
-
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
205
|
-
{/if}
|
|
206
|
-
</div>
|
|
207
|
-
{/each}
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
{:else if workspaceStyle === 'crm'}
|
|
211
|
-
<div class="grid gap-3 md:grid-cols-3">
|
|
212
|
-
{#each lanes as lane (lane.label)}
|
|
213
|
-
<div class="rounded-lg border bg-muted/25 p-3">
|
|
214
|
-
<div class="flex items-center justify-between gap-3">
|
|
215
|
-
<p class="text-sm font-semibold">{lane.label}</p>
|
|
216
|
-
<Badge variant="outline">{lane.value}</Badge>
|
|
217
|
-
</div>
|
|
218
|
-
{#if lane.hint}
|
|
219
|
-
<p class="mt-2 text-xs text-muted-foreground">{lane.hint}</p>
|
|
220
|
-
{/if}
|
|
221
|
-
</div>
|
|
222
|
-
{/each}
|
|
223
|
-
</div>
|
|
224
|
-
<div class="mt-4 grid gap-3">
|
|
225
|
-
{#each highlights as item (item.title)}
|
|
226
|
-
<div class="rounded-lg border-l-4 border-l-primary bg-muted/20 px-4 py-3">
|
|
227
|
-
<div class="flex items-center justify-between gap-3">
|
|
228
|
-
<p class="font-medium">{item.title}</p>
|
|
229
|
-
{#if item.badge}
|
|
230
|
-
<Badge>{item.badge}</Badge>
|
|
231
|
-
{/if}
|
|
232
|
-
</div>
|
|
233
|
-
{#if item.description}
|
|
234
|
-
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
235
|
-
{/if}
|
|
236
|
-
{#if item.meta}
|
|
237
|
-
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
238
|
-
{/if}
|
|
239
|
-
</div>
|
|
240
|
-
{/each}
|
|
241
|
-
</div>
|
|
242
|
-
{:else if workspaceStyle === 'property' || workspaceStyle === 'store'}
|
|
243
|
-
<div class="grid gap-3 md:grid-cols-2">
|
|
244
|
-
{#each highlights as item (item.title)}
|
|
245
|
-
<div class="rounded-xl border bg-gradient-to-br from-muted/45 to-background p-4">
|
|
246
|
-
<div class="flex items-center justify-between gap-3">
|
|
247
|
-
<p class="font-medium">{item.title}</p>
|
|
248
|
-
{#if item.badge}
|
|
249
|
-
<Badge>{item.badge}</Badge>
|
|
250
|
-
{/if}
|
|
251
|
-
</div>
|
|
252
|
-
{#if item.description}
|
|
253
|
-
<p class="mt-2 text-sm text-muted-foreground">{item.description}</p>
|
|
254
|
-
{/if}
|
|
255
|
-
{#if item.meta}
|
|
256
|
-
<p class="mt-3 text-xs font-semibold text-primary">{item.meta}</p>
|
|
257
|
-
{/if}
|
|
258
|
-
</div>
|
|
259
|
-
{/each}
|
|
260
|
-
{#each lanes as lane (lane.label)}
|
|
261
|
-
<div class="rounded-xl border bg-card p-4">
|
|
262
|
-
<div class="flex items-center justify-between gap-3">
|
|
263
|
-
<p class="font-medium">{lane.label}</p>
|
|
264
|
-
<Badge variant="outline">{lane.value}</Badge>
|
|
265
|
-
</div>
|
|
266
|
-
{#if lane.hint}
|
|
267
|
-
<p class="mt-2 text-xs text-muted-foreground">{lane.hint}</p>
|
|
268
|
-
{/if}
|
|
269
|
-
</div>
|
|
270
|
-
{/each}
|
|
271
|
-
</div>
|
|
272
|
-
{:else}
|
|
273
|
-
<div class="space-y-3">
|
|
274
|
-
{#each highlights as item (item.title)}
|
|
275
|
-
<div class="py-3">
|
|
276
|
-
<div class="flex items-center justify-between gap-3">
|
|
277
|
-
<p class="font-medium">{item.title}</p>
|
|
278
|
-
{#if item.badge}
|
|
279
|
-
<Badge>{item.badge}</Badge>
|
|
280
|
-
{/if}
|
|
281
|
-
</div>
|
|
282
|
-
{#if item.description}
|
|
283
|
-
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
284
|
-
{/if}
|
|
285
|
-
{#if item.meta}
|
|
286
|
-
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
287
|
-
{/if}
|
|
288
|
-
</div>
|
|
289
|
-
{/each}
|
|
290
|
-
</div>
|
|
291
|
-
{/if}
|
|
292
|
-
</Card.Content>
|
|
293
|
-
</Card.Root>
|
|
294
|
-
{/if}
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
<AutoTable {resourceName} />
|
|
298
|
-
</div>
|
|
90
|
+
{#if workspaceStyle === 'inventory'}
|
|
91
|
+
<section class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_18rem]" data-svadmin-inventory-layout>
|
|
92
|
+
<div class="space-y-5"><div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="bg-card p-4"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-2 text-2xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div><AutoTable {resourceName} /></div>
|
|
93
|
+
<aside class="space-y-5"><section><h2 class="text-sm font-semibold text-foreground">{tableLabel}</h2><p class="mt-1 text-sm text-muted-foreground">{tableDescription}</p><div class="mt-3 divide-y divide-border border-y border-border">{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3 py-3"><div><p class="text-sm font-medium text-foreground">{lane.label}</p>{#if lane.hint}<p class="text-xs text-muted-foreground">{lane.hint}</p>{/if}</div><Badge variant="outline">{lane.value}</Badge></div>{:else}<p class="py-3 text-sm text-muted-foreground">{emptyLanesText}</p>{/each}</div></section><section><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2><div class="mt-3 space-y-2">{#each highlights as item (item.title)}<article class="rounded-lg border border-border bg-card p-3"><div class="flex items-start justify-between gap-3"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>{/if}</article>{/each}</div></section></aside>
|
|
94
|
+
</section>
|
|
95
|
+
{:else if workspaceStyle === 'operations'}
|
|
96
|
+
<section class="space-y-5" data-svadmin-operations-layout>
|
|
97
|
+
<div class="grid gap-4 lg:grid-cols-[1fr_1.4fr]"><div class="rounded-lg border border-border bg-card p-4"><h2 class="text-sm font-semibold text-foreground">{tableLabel}</h2><div class="mt-4 space-y-4">{#each lanes as lane (lane.label)}<div><div class="flex items-center justify-between gap-3 text-sm"><span class="font-medium">{lane.label}</span><span class="text-muted-foreground">{lane.value}</span></div><div class="mt-2 h-1.5 overflow-hidden rounded-full bg-muted"><div class="h-full rounded-full bg-primary" style:width={`${Math.min(100, Math.max(12, Number(lane.value) * 8))}%`}></div></div>{#if lane.hint}<p class="mt-1 text-xs text-muted-foreground">{lane.hint}</p>{/if}</div>{/each}</div></div><div class="rounded-lg border border-border bg-card"><div class="border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2></div><div class="divide-y divide-border">{#each highlights as item (item.title)}<div class="grid gap-2 px-4 py-3 sm:grid-cols-[1fr_auto]"><div><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}</div><div class="flex items-center gap-2">{#if item.badge}<Badge variant="outline">{item.badge}</Badge>{/if}{#if item.meta}<span class="text-xs text-muted-foreground">{item.meta}</span>{/if}</div></div>{/each}</div></div></div>
|
|
98
|
+
<div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="flex items-center justify-between gap-3 bg-card p-4"><div><p class="text-sm text-muted-foreground">{metric.label}</p>{#if metric.hint}<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>{/if}</div><p class="text-xl font-semibold text-foreground">{metric.value}</p></div>{/each}</div>
|
|
99
|
+
<AutoTable {resourceName} />
|
|
100
|
+
</section>
|
|
101
|
+
{:else if workspaceStyle === 'orders'}
|
|
102
|
+
<section class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_20rem]" data-svadmin-orders-layout>
|
|
103
|
+
<div class="space-y-5"><div class="grid gap-3 sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="border-l-2 border-primary pl-3"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-2xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div><AutoTable {resourceName} /></div>
|
|
104
|
+
<aside class="rounded-lg border border-border bg-card"><div class="border-b border-border p-4"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2><p class="mt-1 text-xs text-muted-foreground">{tableDescription}</p></div><div class="divide-y divide-border">{#each highlights as item (item.title)}<div class="p-4"><div class="flex items-center justify-between gap-2"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge>{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</div>{/each}{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3 p-4"><div><p class="text-sm font-medium">{lane.label}</p>{#if lane.hint}<p class="text-xs text-muted-foreground">{lane.hint}</p>{/if}</div><Badge variant="outline">{lane.value}</Badge></div>{/each}</div></aside>
|
|
105
|
+
</section>
|
|
106
|
+
{:else if workspaceStyle === 'people'}
|
|
107
|
+
<section class="grid gap-5 xl:grid-cols-[18rem_minmax(0,1fr)]" data-svadmin-people-layout>
|
|
108
|
+
<aside class="space-y-5 rounded-lg border border-border bg-card p-4">
|
|
109
|
+
<div><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2><p class="mt-1 text-xs leading-5 text-muted-foreground">{tableDescription}</p></div>
|
|
110
|
+
<div class="grid gap-px overflow-hidden rounded-md border border-border bg-border sm:grid-cols-3 xl:grid-cols-1">{#each metrics as metric (metric.label)}<div class="bg-card py-3"><div class="flex items-center justify-between gap-3"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="text-lg font-semibold text-foreground">{metric.value}</p></div>{#if metric.hint}<p class="text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div>
|
|
111
|
+
<div class="space-y-3">{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3"><div><p class="text-sm font-medium text-foreground">{lane.label}</p>{#if lane.hint}<p class="text-xs text-muted-foreground">{lane.hint}</p>{/if}</div><Badge variant="outline">{lane.value}</Badge></div>{/each}</div>
|
|
112
|
+
</aside>
|
|
113
|
+
<div class="space-y-5"><div class="divide-y divide-border rounded-lg border border-border bg-card">{#each highlights as item (item.title)}<article class="flex items-start justify-between gap-4 p-4"><div><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</div>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</article>{/each}</div><AutoTable {resourceName} /></div>
|
|
114
|
+
</section>
|
|
115
|
+
{:else if workspaceStyle === 'calendar'}
|
|
116
|
+
<section class="space-y-5" data-svadmin-calendar-layout>
|
|
117
|
+
<div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="bg-card p-4"><p class="text-xs text-muted-foreground">{metric.label}</p><p class="mt-1 text-xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div>
|
|
118
|
+
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_17rem]"><div class="rounded-lg border border-border bg-card"><div class="border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2></div><div class="divide-y divide-border">{#each highlights as item (item.title)}<article class="grid gap-2 p-4 sm:grid-cols-[5rem_minmax(0,1fr)_auto]"><p class="text-xs font-medium text-primary">{item.meta}</p><div><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}</div>{#if item.badge}<Badge variant="outline">{item.badge}</Badge>{/if}</article>{/each}</div></div><aside class="space-y-3">{#each lanes as lane (lane.label)}<div class="rounded-lg border border-border bg-card p-4"><div class="flex items-center justify-between gap-3"><p class="text-sm font-medium text-foreground">{lane.label}</p><Badge variant="outline">{lane.value}</Badge></div>{#if lane.hint}<p class="mt-1 text-xs text-muted-foreground">{lane.hint}</p>{/if}</div>{/each}</aside></div>
|
|
119
|
+
<AutoTable {resourceName} />
|
|
120
|
+
</section>
|
|
121
|
+
{:else if workspaceStyle === 'crm'}
|
|
122
|
+
<section class="space-y-5" data-svadmin-crm-layout>
|
|
123
|
+
<div class="grid gap-4 sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="border-l-2 border-primary pl-3"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-2xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div>
|
|
124
|
+
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]"><div class="grid gap-4 sm:grid-cols-3">{#each lanes as lane (lane.label)}<section class="rounded-lg border border-border bg-muted/15"><div class="flex items-center justify-between border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{lane.label}</h2><Badge variant="outline">{lane.value}</Badge></div>{#if lane.hint}<p class="p-4 text-xs leading-5 text-muted-foreground">{lane.hint}</p>{/if}</section>{/each}</div><aside class="divide-y divide-border rounded-lg border border-border bg-card">{#each highlights as item (item.title)}<article class="p-4"><div class="flex items-start justify-between gap-2"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</article>{/each}</aside></div>
|
|
125
|
+
<AutoTable {resourceName} />
|
|
126
|
+
</section>
|
|
127
|
+
{:else if workspaceStyle === 'property'}
|
|
128
|
+
<section class="space-y-5" data-svadmin-property-layout>
|
|
129
|
+
<div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_18rem]"><div class="grid gap-4 md:grid-cols-2">{#each highlights as item (item.title)}<Card.Root><Card.Content class="p-4"><div class="flex aspect-[16/7] items-center justify-center rounded-md bg-muted text-primary">{#if Icon}<Icon class="size-7" />{/if}</div><div class="mt-4 flex items-start justify-between gap-3"><div><h2 class="text-sm font-semibold text-foreground">{item.title}</h2>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}</div>{#if item.badge}<Badge>{item.badge}</Badge>{/if}</div>{#if item.meta}<p class="mt-3 text-xs font-medium text-primary">{item.meta}</p>{/if}</Card.Content></Card.Root>{/each}</div><aside class="space-y-4">{#each metrics as metric (metric.label)}<div class="border-b border-border pb-4"><div class="flex items-center justify-between gap-3"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="text-lg font-semibold text-foreground">{metric.value}</p></div>{#if metric.hint}<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3"><span class="text-sm text-muted-foreground">{lane.label}</span><Badge variant="outline">{lane.value}</Badge></div>{/each}</aside></div>
|
|
130
|
+
<AutoTable {resourceName} />
|
|
131
|
+
</section>
|
|
132
|
+
{:else if workspaceStyle === 'ai'}
|
|
133
|
+
<section class="grid min-h-[30rem] overflow-hidden rounded-lg border border-border bg-card lg:grid-cols-[16rem_minmax(0,1fr)]" data-svadmin-ai-layout>
|
|
134
|
+
<aside class="border-b border-border bg-muted/20 p-4 lg:border-b-0 lg:border-r"><h2 class="text-sm font-semibold text-foreground">{tableLabel}</h2><p class="mt-1 text-xs leading-5 text-muted-foreground">{tableDescription}</p><div class="mt-4 space-y-2">{#each lanes as lane (lane.label)}<div class="rounded-md border border-border bg-card px-3 py-2"><div class="flex items-center justify-between gap-3"><p class="text-sm font-medium text-foreground">{lane.label}</p><Badge variant="outline">{lane.value}</Badge></div>{#if lane.hint}<p class="mt-1 text-xs text-muted-foreground">{lane.hint}</p>{/if}</div>{/each}</div></aside>
|
|
135
|
+
<div class="min-w-0"><div class="border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2></div><div class="space-y-3 p-4">{#each highlights as item (item.title)}<article class="max-w-2xl rounded-lg border border-border bg-muted/15 p-4"><div class="flex items-start justify-between gap-3"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-2 text-sm leading-6 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</article>{/each}</div><div class="border-t border-border p-4"><AutoTable {resourceName} /></div></div>
|
|
136
|
+
</section>
|
|
137
|
+
{:else if workspaceStyle === 'communications'}
|
|
138
|
+
<section class="grid min-h-[32rem] overflow-hidden rounded-lg border border-border bg-card lg:grid-cols-[15rem_minmax(0,1fr)]" data-svadmin-communications-layout>
|
|
139
|
+
<nav class="border-b border-border bg-muted/20 p-3 lg:border-b-0 lg:border-r">{#each lanes as lane (lane.label)}<button class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-card"><span><span class="block font-medium text-foreground">{lane.label}</span>{#if lane.hint}<span class="text-xs text-muted-foreground">{lane.hint}</span>{/if}</span><Badge variant="outline">{lane.value}</Badge></button>{/each}</nav>
|
|
140
|
+
<div class="min-w-0"><div class="border-b border-border p-4"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2></div><div class="divide-y divide-border">{#each highlights as item (item.title)}<article class="grid gap-3 p-4 sm:grid-cols-[1fr_auto]"><div><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.description}<p class="mt-1 text-sm text-muted-foreground">{item.description}</p>{/if}</div><div class="flex items-center gap-2">{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}{#if item.meta}<span class="text-xs text-muted-foreground">{item.meta}</span>{/if}</div></article>{/each}</div><div class="border-t border-border p-4"><AutoTable {resourceName} /></div></div>
|
|
141
|
+
</section>
|
|
142
|
+
{:else if workspaceStyle === 'store'}
|
|
143
|
+
<section class="space-y-5" data-svadmin-store-layout><div class="flex flex-col gap-4 border-b border-border pb-4 sm:flex-row sm:items-end sm:justify-between"><div><h2 class="text-base font-semibold text-foreground">{highlightsLabel}</h2><p class="mt-1 text-sm text-muted-foreground">{tableDescription}</p></div><div class="flex gap-5">{#each metrics as metric (metric.label)}<div><p class="text-xs text-muted-foreground">{metric.label}</p><p class="text-lg font-semibold text-foreground">{metric.value}</p></div>{/each}</div></div><div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{#each highlights as item (item.title)}<Card.Root><Card.Content class="p-4"><div class="flex aspect-[16/8] items-center justify-center rounded-md bg-muted text-primary">{#if Icon}<Icon class="size-8" />{/if}</div><div class="mt-4 flex items-start justify-between gap-3"><div><h3 class="text-sm font-semibold text-foreground">{item.title}</h3>{#if item.description}<p class="mt-1 text-sm text-muted-foreground">{item.description}</p>{/if}</div>{#if item.badge}<Badge>{item.badge}</Badge>{/if}</div>{#if item.meta}<p class="mt-3 text-xs font-medium text-primary">{item.meta}</p>{/if}</Card.Content></Card.Root>{/each}</div><div class="grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)]"><aside class="rounded-lg border border-border bg-card p-4"><h2 class="text-sm font-semibold text-foreground">{tableLabel}</h2><div class="mt-3 space-y-3">{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3"><span class="text-sm text-muted-foreground">{lane.label}</span><Badge variant="outline">{lane.value}</Badge></div>{/each}</div></aside><AutoTable {resourceName} /></div></section>
|
|
144
|
+
{:else if workspaceStyle === 'planning'}
|
|
145
|
+
<section class="space-y-5" data-svadmin-planning-layout><div class="grid gap-4 lg:grid-cols-3">{#each lanes as lane, laneIndex (lane.label)}<section class="rounded-lg border border-border bg-muted/15"><div class="flex items-center justify-between border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{lane.label}</h2><Badge variant="outline">{lane.value}</Badge></div><div class="space-y-3 p-3">{#each highlights.filter((_, itemIndex) => itemIndex % lanes.length === laneIndex) as item (item.title)}<article class="rounded-md border border-border bg-card p-3"><div class="flex items-start justify-between gap-2"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</article>{/each}</div></section>{/each}</div><div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="bg-card p-4"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div><AutoTable {resourceName} /></section>
|
|
146
|
+
{:else if workspaceStyle === 'generation'}
|
|
147
|
+
<section class="grid gap-5 xl:grid-cols-[19rem_minmax(0,1fr)]" data-svadmin-generation-layout>
|
|
148
|
+
<aside class="rounded-lg border border-border bg-card p-4"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2><div class="mt-4 space-y-4">{#each lanes as lane, index (lane.label)}<div class="flex items-start gap-3"><span class="flex size-7 shrink-0 items-center justify-center rounded-md border border-border bg-muted text-xs font-semibold text-foreground">{index + 1}</span><div><div class="flex items-center gap-2"><p class="text-sm font-medium text-foreground">{lane.label}</p><Badge variant="outline">{lane.value}</Badge></div>{#if lane.hint}<p class="mt-1 text-xs leading-5 text-muted-foreground">{lane.hint}</p>{/if}</div></div>{/each}</div></aside>
|
|
149
|
+
<div class="space-y-5"><div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="bg-card p-4"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-xl font-semibold text-foreground">{metric.value}</p>{#if metric.hint}<p class="text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}</div><div class="grid gap-3 md:grid-cols-3">{#each highlights as item (item.title)}<article class="rounded-lg border border-border bg-card p-4"><div class="flex items-start justify-between gap-2"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge variant="secondary">{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-2 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}</article>{/each}</div><AutoTable {resourceName} /></div>
|
|
150
|
+
</section>
|
|
151
|
+
{:else if workspaceStyle === 'billing'}
|
|
152
|
+
<section class="space-y-5" data-svadmin-billing-layout><div class="grid gap-4 md:grid-cols-3">{#each highlights as item (item.title)}<Card.Root><Card.Header><div class="flex items-center justify-between gap-3"><Card.Title class="text-base">{item.title}</Card.Title>{#if item.badge}<Badge>{item.badge}</Badge>{/if}</div>{#if item.description}<Card.Description>{item.description}</Card.Description>{/if}</Card.Header><Card.Content>{#if item.meta}<p class="text-2xl font-semibold text-foreground">{item.meta}</p>{/if}<Button variant="outline" size="sm" class="mt-4 w-full">{actionLabel}</Button></Card.Content></Card.Root>{/each}</div><div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_18rem]"><AutoTable {resourceName} /><aside class="space-y-4">{#each metrics as metric (metric.label)}<div class="border-b border-border pb-4"><div class="flex items-center justify-between gap-3"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="text-lg font-semibold text-foreground">{metric.value}</p></div>{#if metric.hint}<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>{/if}</div>{/each}{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3"><span class="text-sm text-muted-foreground">{lane.label}</span><Badge variant="outline">{lane.value}</Badge></div>{/each}</aside></div></section>
|
|
153
|
+
{:else if workspaceStyle === 'security'}
|
|
154
|
+
<section class="space-y-5" data-svadmin-security-layout><div class="grid gap-4 lg:grid-cols-[0.7fr_1.3fr]"><div class="rounded-lg border border-border bg-card p-5"><p class="text-sm font-medium text-muted-foreground">{metrics[0]?.label}</p><p class="mt-3 text-3xl font-semibold text-foreground">{metrics[0]?.value}</p><p class="mt-1 text-sm text-muted-foreground">{metrics[0]?.hint}</p><div class="mt-5 space-y-3">{#each lanes as lane (lane.label)}<div class="flex items-center justify-between gap-3"><span class="text-sm text-muted-foreground">{lane.label}</span><Badge variant="outline">{lane.value}</Badge></div>{/each}</div></div><div class="rounded-lg border border-border bg-card"><div class="border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{highlightsLabel}</h2></div><div class="divide-y divide-border">{#each highlights as item (item.title)}<div class="flex items-start justify-between gap-3 p-4"><div><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-2 text-xs text-primary">{item.meta}</p>{/if}</div>{#if item.badge}<Badge variant="outline">{item.badge}</Badge>{/if}</div>{/each}</div></div></div><AutoTable {resourceName} /></section>
|
|
155
|
+
{:else if workspaceStyle === 'referral'}
|
|
156
|
+
<section class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]" data-svadmin-referral-layout><div class="space-y-5"><div class="rounded-lg border border-border bg-card p-5"><h2 class="text-base font-semibold text-foreground">{highlightsLabel}</h2><p class="mt-1 text-sm text-muted-foreground">{tableDescription}</p><div class="mt-5 grid gap-3 sm:grid-cols-3">{#each metrics as metric (metric.label)}<div><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-2xl font-semibold text-foreground">{metric.value}</p><p class="text-xs text-muted-foreground">{metric.hint}</p></div>{/each}</div></div><AutoTable {resourceName} /></div><aside class="space-y-3">{#each highlights as item (item.title)}<article class="rounded-lg border border-border bg-card p-4"><div class="flex items-center justify-between gap-3"><p class="text-sm font-medium text-foreground">{item.title}</p>{#if item.badge}<Badge>{item.badge}</Badge>{/if}</div>{#if item.description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{item.description}</p>{/if}{#if item.meta}<p class="mt-3 rounded-md bg-muted px-3 py-2 font-mono text-xs text-foreground">{item.meta}</p>{/if}</article>{/each}</aside></section>
|
|
157
|
+
{:else}
|
|
158
|
+
<section class="space-y-4" data-svadmin-default-layout><div class="grid gap-px overflow-hidden rounded-lg border border-border bg-border sm:grid-cols-3">{#each metrics as metric (metric.label)}<div class="bg-card p-4"><p class="text-sm text-muted-foreground">{metric.label}</p><p class="mt-1 text-xl font-semibold text-foreground">{metric.value}</p></div>{/each}</div><AutoTable {resourceName} /></section>
|
|
159
|
+
{/if}
|
|
299
160
|
</div>
|
|
@@ -15,7 +15,7 @@ interface HighlightItem {
|
|
|
15
15
|
meta?: string;
|
|
16
16
|
badge?: string;
|
|
17
17
|
}
|
|
18
|
-
type WorkspaceStyle = 'inventory' | 'operations' | 'people' | 'calendar' | 'communications' | 'crm' | 'property' | 'ai' | 'store';
|
|
18
|
+
type WorkspaceStyle = 'inventory' | 'operations' | 'orders' | 'people' | 'calendar' | 'communications' | 'crm' | 'property' | 'ai' | 'store' | 'planning' | 'generation' | 'billing' | 'security' | 'referral';
|
|
19
19
|
interface Props {
|
|
20
20
|
resourceName: string;
|
|
21
21
|
eyebrow: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { useNotification } from '@svadmin/core';
|
|
2
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
4
|
|
|
4
|
-
import { AlertTriangle,
|
|
5
|
+
import { AlertTriangle, KeyRound, Monitor, Shield, ShieldAlert, ShieldCheck, Smartphone, Trash2 } from '@lucide/svelte';
|
|
5
6
|
import type { Component } from 'svelte';
|
|
6
7
|
import * as Alert from './ui/alert/index.js';
|
|
7
8
|
import * as Card from './ui/card/index.js';
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
import PasswordInput from './PasswordInput.svelte';
|
|
12
13
|
|
|
13
14
|
const i18n = useTranslation();
|
|
15
|
+
const notification = useNotification();
|
|
14
16
|
|
|
15
17
|
interface SessionInfo {
|
|
16
18
|
id: string;
|
|
@@ -32,13 +34,11 @@
|
|
|
32
34
|
let currentPassword = $state('');
|
|
33
35
|
let newPassword = $state('');
|
|
34
36
|
let confirmPassword = $state('');
|
|
35
|
-
let passwordSuccessMessage = $state('');
|
|
36
37
|
let passwordErrorMessage = $state('');
|
|
37
38
|
let lastChanged = $state('3 months ago');
|
|
38
39
|
|
|
39
40
|
function handlePasswordChange(event: SubmitEvent) {
|
|
40
41
|
event.preventDefault();
|
|
41
|
-
passwordSuccessMessage = '';
|
|
42
42
|
passwordErrorMessage = '';
|
|
43
43
|
|
|
44
44
|
if (!currentPassword) {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
notification.success(i18n.t('profile.passwordChanged'), 3000);
|
|
58
58
|
lastChanged = 'Just now';
|
|
59
59
|
currentPassword = '';
|
|
60
60
|
newPassword = '';
|
|
@@ -111,12 +111,6 @@
|
|
|
111
111
|
</Card.CardHeader>
|
|
112
112
|
<Card.CardContent>
|
|
113
113
|
<form onsubmit={handlePasswordChange} class="max-w-sm space-y-4">
|
|
114
|
-
{#if passwordSuccessMessage}
|
|
115
|
-
<Alert.Root class="border-success/30 bg-success/5 text-success">
|
|
116
|
-
<CheckCircle2 class="h-4 w-4" />
|
|
117
|
-
<Alert.Description>{passwordSuccessMessage}</Alert.Description>
|
|
118
|
-
</Alert.Root>
|
|
119
|
-
{/if}
|
|
120
114
|
{#if passwordErrorMessage}
|
|
121
115
|
<Alert.Root variant="destructive">
|
|
122
116
|
<AlertTriangle class="h-4 w-4" />
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
const queue = getToastQueue();
|
|
21
21
|
if (queue.length > 0) {
|
|
22
22
|
for (const t of queue) {
|
|
23
|
+
const options = { duration: t.duration, id: t.key ?? t.id };
|
|
23
24
|
switch (t.type) {
|
|
24
|
-
case 'success': sonner.success(t.message,
|
|
25
|
-
case 'error': sonner.error(t.message,
|
|
26
|
-
case 'warning': sonner.warning(t.message,
|
|
27
|
-
case 'info': sonner.info(t.message,
|
|
25
|
+
case 'success': sonner.success(t.message, options); break;
|
|
26
|
+
case 'error': sonner.error(t.message, options); break;
|
|
27
|
+
case 'warning': sonner.warning(t.message, options); break;
|
|
28
|
+
case 'info': sonner.info(t.message, options); break;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
consumeToastQueue();
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
position: "top-right",
|
|
47
48
|
richColors: true,
|
|
48
49
|
closeButton: true,
|
|
49
|
-
expand:
|
|
50
|
+
expand: false,
|
|
51
|
+
visibleToasts: 3,
|
|
50
52
|
theme,
|
|
51
53
|
toastOptions: {
|
|
52
54
|
classes: {
|
|
@@ -1,43 +1,64 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import { ArrowRight,
|
|
3
|
+
import { ArrowRight, Building2, Check, KeyRound, Palette, Plug, ShieldCheck, UserRound, Users } from '@lucide/svelte';
|
|
4
4
|
import * as Card from '../ui/card/index.js';
|
|
5
5
|
import { Badge } from '../ui/badge/index.js';
|
|
6
6
|
import { Button } from '../ui/button/index.js';
|
|
7
7
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
8
8
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
|
-
import TwoFactorStepper from '../content/TwoFactorStepper.svelte';
|
|
10
9
|
|
|
11
10
|
const i18n = useTranslation();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ title:
|
|
16
|
-
{ title:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
let completed = $state<string[]>(['profile', 'security']);
|
|
12
|
+
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
13
|
+
const entries = $derived([
|
|
14
|
+
{ id: 'profile', title: isZh ? '个人资料' : 'User profile', description: isZh ? '维护联系人、工作状态、文件和日历连接。' : 'Maintain contact details, work status, files, and calendar connections.', href: '#/account/home/user-profile', icon: UserRound },
|
|
15
|
+
{ id: 'company', title: isZh ? '企业资料' : 'Company profile', description: isZh ? '配置企业信息、品牌资料、成员和数据导入。' : 'Configure company details, branding, members, and data imports.', href: '#/account/home/company-profile', icon: Building2 },
|
|
16
|
+
{ id: 'members', title: isZh ? '成员与邀请' : 'Members and invites', description: isZh ? '邀请成员、分配角色或从 CSV 批量导入。' : 'Invite members, assign roles, or import a CSV roster.', href: '#/account/members/team-members', icon: Users },
|
|
17
|
+
{ id: 'security', title: isZh ? '安全设置' : 'Security setup', description: isZh ? '启用两步验证并检查账户安全事件。' : 'Enable two-factor authentication and review account security events.', href: '#/authentication/branded/2fa', icon: ShieldCheck },
|
|
18
|
+
{ id: 'appearance', title: isZh ? '外观与偏好' : 'Appearance and preferences', description: isZh ? '设置主题、密度、语言和辅助功能。' : 'Set theme, density, language, and accessibility preferences.', href: '#/account/appearance', icon: Palette },
|
|
19
|
+
{ id: 'integrations', title: isZh ? '集成与 API' : 'Integrations and API', description: isZh ? '连接外部服务,并管理 API 密钥和 Webhook。' : 'Connect external services and manage API keys and webhooks.', href: '#/account/integrations', icon: Plug },
|
|
20
|
+
]);
|
|
21
|
+
const progress = $derived(Math.round(completed.length / entries.length * 100));
|
|
22
|
+
|
|
23
|
+
function toggleCompleted(id: string) {
|
|
24
|
+
completed = completed.includes(id) ? completed.filter((item) => item !== id) : [...completed, id];
|
|
25
25
|
}
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
|
-
<ContentPageShell pageId="account-get-started" width="
|
|
29
|
-
<div class="flex
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
<ContentPageShell pageId="account-get-started" width="wide">
|
|
29
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
30
|
+
<ContentPageHeader title={i18n.t('account.getStarted')} description={isZh ? '从账户功能入口快速完成组织、成员、安全和集成配置。' : 'Use the account hub to complete organization, member, security, and integration setup.'} />
|
|
31
|
+
<Badge variant="outline">{progress}% {isZh ? '已完成' : 'complete'}</Badge>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_19rem]">
|
|
35
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
36
|
+
{#each entries as entry (entry.id)}
|
|
37
|
+
<Card.Card class="flex min-h-52 flex-col">
|
|
38
|
+
<Card.CardContent class="flex flex-1 flex-col p-5">
|
|
39
|
+
<div class="flex items-start justify-between gap-3">
|
|
40
|
+
<span class="flex size-9 items-center justify-center rounded-md border border-border bg-card text-primary"><entry.icon class="size-4" /></span>
|
|
41
|
+
<button type="button" class="flex size-8 items-center justify-center rounded-md border border-border text-muted-foreground hover:text-foreground" aria-label={isZh ? '切换完成状态' : 'Toggle completion'} onclick={() => toggleCompleted(entry.id)}>
|
|
42
|
+
{#if completed.includes(entry.id)}<Check class="size-4 text-success" />{/if}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
<h2 class="mt-4 text-base font-semibold text-foreground">{entry.title}</h2>
|
|
46
|
+
<p class="mt-1 flex-1 text-sm leading-6 text-muted-foreground">{entry.description}</p>
|
|
47
|
+
<Button href={entry.href} variant="link" class="mt-4 h-auto justify-start p-0">{isZh ? '打开设置' : 'Open settings'}<ArrowRight class="size-3.5" /></Button>
|
|
48
|
+
</Card.CardContent>
|
|
49
|
+
</Card.Card>
|
|
50
|
+
{/each}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<aside class="space-y-5">
|
|
54
|
+
<section class="rounded-lg border border-border bg-card p-5">
|
|
55
|
+
<div class="flex items-center gap-3"><span class="flex size-9 items-center justify-center rounded-md bg-primary/10 text-primary"><KeyRound class="size-4" /></span><div><h2 class="text-sm font-semibold text-foreground">{isZh ? '账户准备度' : 'Account readiness'}</h2><p class="text-xs text-muted-foreground">{completed.length} / {entries.length}</p></div></div>
|
|
56
|
+
<div class="mt-4 h-2 overflow-hidden rounded-full bg-muted"><div class="h-full rounded-full bg-primary" style:width={`${progress}%`}></div></div>
|
|
57
|
+
<div class="mt-4 divide-y divide-border border-y border-border">
|
|
58
|
+
{#each entries as entry (entry.id)}<div class="flex items-center justify-between gap-3 py-3 text-sm"><span class="text-muted-foreground">{entry.title}</span>{#if completed.includes(entry.id)}<Badge variant="secondary">{isZh ? '完成' : 'Done'}</Badge>{:else}<span class="text-xs text-muted-foreground">{isZh ? '待配置' : 'Pending'}</span>{/if}</div>{/each}
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
<Button class="w-full" onclick={() => completed = entries.map((entry) => entry.id)}>{isZh ? '标记全部完成' : 'Mark all complete'}</Button>
|
|
62
|
+
</aside>
|
|
42
63
|
</div>
|
|
43
64
|
</ContentPageShell>
|
|
@@ -1,106 +1,63 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
+
import { ArrowRight, Check, Download, Mail, Upload, UserPlus, Users } from '@lucide/svelte';
|
|
3
4
|
import * as Card from '../ui/card/index.js';
|
|
4
5
|
import { Button } from '../ui/button/index.js';
|
|
5
|
-
import { Badge } from '../ui/badge/index.js';
|
|
6
|
-
import { Avatar } from '../ui/avatar/index.js';
|
|
7
6
|
import { Input } from '../ui/input/index.js';
|
|
8
|
-
import {
|
|
7
|
+
import { Badge } from '../ui/badge/index.js';
|
|
8
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
10
|
|
|
10
11
|
const i18n = useTranslation();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
let inviteOpen = $state(false);
|
|
13
|
+
let email = $state('');
|
|
14
|
+
let invited = $state<string[]>([]);
|
|
15
|
+
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
16
|
+
|
|
17
|
+
function sendInvite() {
|
|
18
|
+
const normalized = email.trim();
|
|
19
|
+
if (!normalized || invited.includes(normalized)) return;
|
|
20
|
+
invited = [...invited, normalized];
|
|
21
|
+
email = '';
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
let searchQuery = $state('');
|
|
23
|
-
|
|
24
|
-
let members = $state<Member[]>([
|
|
25
|
-
{ id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', status: 'active', joinedAt: '2023-01-15' },
|
|
26
|
-
{ id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', status: 'active', joinedAt: '2023-03-22' },
|
|
27
|
-
{ id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', status: 'invited', joinedAt: '2024-01-10' },
|
|
28
|
-
{ id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', status: 'active', joinedAt: '2023-06-05' },
|
|
29
|
-
{ id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', status: 'inactive', joinedAt: '2022-11-20' },
|
|
30
|
-
{ id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', status: 'active', joinedAt: '2023-02-14' },
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const filteredMembers = $derived(
|
|
34
|
-
searchQuery
|
|
35
|
-
? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
36
|
-
: members
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const statusConfig = {
|
|
40
|
-
'active': { icon: CheckCircle2, color: 'text-green-500', label: i18n.t('account.active') },
|
|
41
|
-
'invited': { icon: Clock, color: 'text-amber-500', label: i18n.t('account.invited') },
|
|
42
|
-
'inactive': { icon: XCircle, color: 'text-muted-foreground', label: i18n.t('account.inactive') },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
46
23
|
</script>
|
|
47
24
|
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
<div>
|
|
51
|
-
<h2 class="text-xl font-semibold text-foreground">{i18n.t('account.membersStarter')}</h2>
|
|
52
|
-
<p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.membersStarterDescription')}</p>
|
|
53
|
-
</div>
|
|
54
|
-
<Button size="sm">
|
|
55
|
-
<UserPlus class="h-4 w-4 mr-1" />{i18n.t('account.inviteMember')}
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
25
|
+
<ContentPageShell pageId="account-members-starter" width="wide">
|
|
26
|
+
<ContentPageHeader title={isZh ? '开始组建团队' : 'Start building your team'} description={isZh ? '这是零成员工作区的起始页。邀请第一位成员,或通过 CSV 批量导入。' : 'This is the starting state for a workspace with no members. Invite the first teammate or import a CSV roster.'} />
|
|
58
27
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class="
|
|
66
|
-
|
|
67
|
-
</div>
|
|
28
|
+
<section class="grid min-h-[28rem] overflow-hidden rounded-lg border border-border bg-card lg:grid-cols-[minmax(0,1fr)_21rem]">
|
|
29
|
+
<div class="flex flex-col items-center justify-center px-6 py-12 text-center">
|
|
30
|
+
<span class="flex size-14 items-center justify-center rounded-lg bg-primary/10 text-primary"><Users class="size-6" /></span>
|
|
31
|
+
<Badge variant="outline" class="mt-5">0 {isZh ? '名成员' : 'members'}</Badge>
|
|
32
|
+
<h2 class="mt-4 text-xl font-semibold text-foreground">{isZh ? '邀请你的第一位团队成员' : 'Invite your first team member'}</h2>
|
|
33
|
+
<p class="mt-2 max-w-lg text-sm leading-6 text-muted-foreground">{isZh ? '成员加入后,你可以继续分配角色、设置权限、查看活动状态,并在团队成员页批量管理。' : 'After members join, assign roles, configure access, review activity, and manage them in the full team member table.'}</p>
|
|
34
|
+
<div class="mt-6 flex flex-wrap justify-center gap-2"><Button onclick={() => inviteOpen = true}><UserPlus class="size-4" />{isZh ? '邀请成员' : 'Invite member'}</Button><Button href="#/account/members/import-members" variant="outline"><Upload class="size-4" />{isZh ? '导入 CSV' : 'Import CSV'}</Button></div>
|
|
35
|
+
</div>
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
<div class="flex items-center gap-3 p-4 hover:bg-accent/30 transition-colors">
|
|
76
|
-
<div class="shrink-0">
|
|
77
|
-
{#if member.avatar}
|
|
78
|
-
<Avatar class="h-9 w-9"><img src={member.avatar} alt={member.name} /></Avatar>
|
|
79
|
-
{:else}
|
|
80
|
-
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold">
|
|
81
|
-
{initials(member.name)}
|
|
82
|
-
</div>
|
|
83
|
-
{/if}
|
|
84
|
-
</div>
|
|
85
|
-
<div class="flex-1 min-w-0">
|
|
86
|
-
<div class="flex items-center gap-2">
|
|
87
|
-
<span class="text-sm font-medium text-foreground truncate">{member.name}</span>
|
|
88
|
-
<Badge variant="outline" class="text-[10px] shrink-0">{member.role}</Badge>
|
|
89
|
-
</div>
|
|
90
|
-
<p class="text-xs text-muted-foreground truncate">{member.email}</p>
|
|
91
|
-
</div>
|
|
92
|
-
<div class="shrink-0 flex items-center gap-3">
|
|
93
|
-
<span class="flex items-center gap-1 text-xs {sc.color}">
|
|
94
|
-
<sc.icon class="h-3 w-3" />
|
|
95
|
-
{sc.label}
|
|
96
|
-
</span>
|
|
97
|
-
<Button variant="ghost" size="icon-sm">
|
|
98
|
-
<MoreHorizontal class="h-4 w-4" />
|
|
99
|
-
</Button>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
37
|
+
<aside class="border-t border-border bg-muted/15 p-5 lg:border-l lg:border-t-0">
|
|
38
|
+
<h2 class="text-sm font-semibold text-foreground">{isZh ? '团队准备清单' : 'Team setup checklist'}</h2>
|
|
39
|
+
<div class="mt-4 space-y-4">
|
|
40
|
+
{#each [isZh ? '邀请至少一名管理员' : 'Invite at least one admin', isZh ? '确认默认成员角色' : 'Confirm the default member role', isZh ? '准备成员 CSV 模板' : 'Prepare the member CSV template'] as item, index (item)}
|
|
41
|
+
{@const completed = index === 0 && invited.length > 0}
|
|
42
|
+
<div class="flex items-start gap-3" data-checklist-status={completed ? 'complete' : 'pending'}><span class={'mt-0.5 flex size-6 items-center justify-center rounded-md border ' + (completed ? 'border-success/30 bg-success/10 text-success' : 'border-border text-muted-foreground')}>{#if completed}<Check class="size-3.5" />{:else}<span class="text-xs">{index + 1}</span>{/if}</span><p class="text-sm leading-5 text-muted-foreground">{item}</p></div>
|
|
102
43
|
{/each}
|
|
103
44
|
</div>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</
|
|
45
|
+
<a href="#/account/members/team-members" class="mt-6 flex items-center gap-1 border-t border-border pt-4 text-sm font-medium text-primary">{isZh ? '查看完整成员管理' : 'Open full member management'}<ArrowRight class="size-3.5" /></a>
|
|
46
|
+
<Button variant="outline" size="sm" class="mt-3 w-full"><Download class="size-3.5" />{isZh ? '下载 CSV 模板' : 'Download CSV template'}</Button>
|
|
47
|
+
</aside>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
{#if inviteOpen}
|
|
51
|
+
<Card.Card data-member-invite-panel>
|
|
52
|
+
<Card.CardContent class="grid gap-4 p-5 md:grid-cols-[auto_minmax(0,1fr)_auto] md:items-end">
|
|
53
|
+
<span class="flex size-10 items-center justify-center rounded-md bg-primary/10 text-primary"><Mail class="size-4" /></span>
|
|
54
|
+
<label><span class="text-sm font-medium text-foreground">{isZh ? '成员邮箱' : 'Member email'}</span><Input class="mt-2" type="email" bind:value={email} placeholder="teammate@example.com" /></label>
|
|
55
|
+
<div class="flex gap-2"><Button variant="outline" onclick={() => inviteOpen = false}>{i18n.t('common.cancel')}</Button><Button onclick={sendInvite}>{isZh ? '发送邀请' : 'Send invite'}</Button></div>
|
|
56
|
+
</Card.CardContent>
|
|
57
|
+
</Card.Card>
|
|
58
|
+
{/if}
|
|
59
|
+
|
|
60
|
+
{#if invited.length > 0}
|
|
61
|
+
<section class="rounded-lg border border-border bg-card"><div class="border-b border-border px-4 py-3"><h2 class="text-sm font-semibold text-foreground">{isZh ? '待接受邀请' : 'Pending invitations'}</h2></div><div class="divide-y divide-border">{#each invited as invite (invite)}<div class="flex items-center justify-between gap-3 px-4 py-3"><div><p class="text-sm font-medium text-foreground">{invite}</p><p class="text-xs text-muted-foreground">{isZh ? '刚刚发送' : 'Sent just now'}</p></div><Badge variant="outline">{isZh ? '已邀请' : 'Invited'}</Badge></div>{/each}</div></section>
|
|
62
|
+
{/if}
|
|
63
|
+
</ContentPageShell>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { CircleAlert, Info, TriangleAlert } from '@lucide/svelte';
|
|
4
|
+
import { cn } from '../../utils.js';
|
|
5
|
+
|
|
6
|
+
export type FeedbackNoticeTone = 'info' | 'warning' | 'danger';
|
|
7
|
+
export type FeedbackNoticePriority = 'contextual' | 'blocking';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
message: string;
|
|
11
|
+
tone?: FeedbackNoticeTone;
|
|
12
|
+
priority?: FeedbackNoticePriority;
|
|
13
|
+
action?: Snippet;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
message,
|
|
19
|
+
tone = 'info',
|
|
20
|
+
priority = 'contextual',
|
|
21
|
+
action,
|
|
22
|
+
class: className = '',
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const isBlocking = $derived(priority === 'blocking' || tone === 'danger');
|
|
26
|
+
const toneClass = $derived(
|
|
27
|
+
tone === 'danger'
|
|
28
|
+
? 'border-destructive/40 bg-destructive/5 text-destructive'
|
|
29
|
+
: tone === 'warning'
|
|
30
|
+
? 'border-warning/50 bg-warning/10 text-warning-foreground'
|
|
31
|
+
: 'border-border bg-muted/40 text-foreground',
|
|
32
|
+
);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div
|
|
36
|
+
data-svadmin-feedback-notice
|
|
37
|
+
data-tone={tone}
|
|
38
|
+
data-priority={priority}
|
|
39
|
+
role={isBlocking ? 'alert' : 'status'}
|
|
40
|
+
aria-live={isBlocking ? 'assertive' : 'polite'}
|
|
41
|
+
class={cn(
|
|
42
|
+
'flex w-full flex-col gap-3 rounded-md border px-3 py-2.5 text-sm sm:flex-row sm:items-center',
|
|
43
|
+
isBlocking && 'border-l-2',
|
|
44
|
+
toneClass,
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<div class="flex min-w-0 flex-1 items-start gap-2.5">
|
|
49
|
+
{#if tone === 'danger'}
|
|
50
|
+
<CircleAlert class="mt-0.5 size-4 shrink-0" />
|
|
51
|
+
{:else if tone === 'warning'}
|
|
52
|
+
<TriangleAlert class="mt-0.5 size-4 shrink-0" />
|
|
53
|
+
{:else}
|
|
54
|
+
<Info class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
55
|
+
{/if}
|
|
56
|
+
<p class="min-w-0 leading-5">{message}</p>
|
|
57
|
+
</div>
|
|
58
|
+
{#if action}
|
|
59
|
+
<div class="shrink-0">{@render action()}</div>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type FeedbackNoticeTone = 'info' | 'warning' | 'danger';
|
|
3
|
+
export type FeedbackNoticePriority = 'contextual' | 'blocking';
|
|
4
|
+
interface Props {
|
|
5
|
+
message: string;
|
|
6
|
+
tone?: FeedbackNoticeTone;
|
|
7
|
+
priority?: FeedbackNoticePriority;
|
|
8
|
+
action?: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const FeedbackNotice: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type FeedbackNotice = ReturnType<typeof FeedbackNotice>;
|
|
13
|
+
export default FeedbackNotice;
|
|
@@ -30,3 +30,5 @@ export type { NetworkColumn } from './NetworkTable.types.js';
|
|
|
30
30
|
export { default as OtpInput } from './OtpInput.svelte';
|
|
31
31
|
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
32
32
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
33
|
+
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
34
|
+
export type { FeedbackNoticeTone, FeedbackNoticePriority } from './FeedbackNotice.svelte';
|
|
@@ -20,3 +20,4 @@ export { default as NetworkTable } from './NetworkTable.svelte';
|
|
|
20
20
|
export { default as OtpInput } from './OtpInput.svelte';
|
|
21
21
|
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
22
22
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
23
|
+
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import
|
|
3
|
+
import { Badge } from '../ui/badge/index.js';
|
|
4
4
|
import ProfileCard from './ProfileCard.svelte';
|
|
5
5
|
import ProjectsGrid from './ProjectsGrid.svelte';
|
|
6
6
|
import ActivityTimeline from './ActivityTimeline.svelte';
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
type ProfileTab = 'projects' | 'activity' | 'teams';
|
|
15
15
|
interface Props { variant?: ProfileVariant; initialTab?: ProfileTab; columns?: 2 | 3; showSections?: boolean; }
|
|
16
16
|
let { variant = 'default', initialTab = 'projects', columns = 2, showSections = false }: Props = $props();
|
|
17
|
-
let activeTab = $derived<ProfileTab>(initialTab);
|
|
18
17
|
const i18n = useTranslation();
|
|
18
|
+
const columnLabel = $derived(i18n.locale === 'zh-CN' ? '栏' : 'columns');
|
|
19
|
+
const pageId = $derived(showSections ? `public-profile-${variant}` : `public-profile-${initialTab}-${initialTab === 'projects' ? columns : 'view'}`);
|
|
19
20
|
|
|
20
21
|
const profileData = $derived.by(() => variant === 'company'
|
|
21
22
|
? { name: 'Acme Corporation', tagline: 'Building the future of enterprise software', industry: 'Technology', employees: 1250, founded: '2015', website: 'acme.com', location: 'San Francisco, CA', followers: 8420, following: 312, tags: ['SaaS', 'Enterprise', 'Cloud'], stats: [{ label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('publicProfile.projects'), value: '48' }, { label: i18n.t('publicProfile.teams'), value: '12' }] }
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
: { name: 'Alex Chen', tagline: 'Full-stack developer & open source enthusiast', location: 'Shanghai, CN', website: 'alexchen.dev', joinedDate: '2023', followers: 1234, following: 567, tags: ['TypeScript', 'Svelte', 'Open Source'], stats: [{ label: i18n.t('publicProfile.projects'), value: '24' }, { label: i18n.t('publicProfile.followers'), value: '1.2K' }, { label: i18n.t('publicProfile.following'), value: '567' }] });
|
|
25
26
|
|
|
26
27
|
const projects = referenceDemoData.projects;
|
|
28
|
+
const visibleProjects = $derived(columns === 2 ? projects.slice(0, 6) : projects);
|
|
27
29
|
const activities = $derived.by(() => [
|
|
28
30
|
{ id: '1', type: 'posted' as const, user: profileData.name, content: 'Shipped the new dashboard redesign with real-time analytics support.', timestamp: '2 hours ago' },
|
|
29
31
|
{ id: '2', type: 'commented' as const, user: profileData.name, target: 'API Gateway PR #42', content: 'Looks great, with a few minor suggestions on error handling.', timestamp: '5 hours ago' },
|
|
@@ -32,21 +34,17 @@
|
|
|
32
34
|
const teams = referenceDemoData.teams;
|
|
33
35
|
</script>
|
|
34
36
|
|
|
35
|
-
<ContentPageShell pageId
|
|
37
|
+
<ContentPageShell {pageId} width="wide">
|
|
36
38
|
<ContentPageHeader title={profileData.name} eyebrow={i18n.t('publicProfile.title')} description={profileData.tagline} />
|
|
37
|
-
<ProfileCard {...profileData} {variant} />
|
|
38
39
|
{#if showSections}
|
|
39
|
-
<ProfileVariantSections {variant}
|
|
40
|
+
<div class="grid gap-6 xl:grid-cols-[20rem_minmax(0,1fr)]"><ProfileCard {...profileData} {variant} /><ProfileVariantSections {variant} /></div>
|
|
41
|
+
{:else if initialTab === 'projects'}
|
|
42
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-y border-border py-3"><div class="flex items-center gap-2"><Badge variant="outline">{columns} {columnLabel}</Badge><span class="text-sm text-muted-foreground">{visibleProjects.length} {i18n.t('publicProfile.projects')}</span></div><div class="flex gap-3 text-sm"><a class="text-primary" href="#/public-profile/projects/2-columns">2 {columnLabel}</a><a class="text-primary" href="#/public-profile/projects/3-columns">3 {columnLabel}</a></div></div>
|
|
43
|
+
<div class={columns === 2 ? 'grid gap-6 xl:grid-cols-[20rem_minmax(0,1fr)]' : 'space-y-5'}>{#if columns === 2}<ProfileCard {...profileData} {variant} />{/if}<ProjectsGrid projects={visibleProjects} {columns} /></div>
|
|
44
|
+
{:else if initialTab === 'activity'}
|
|
45
|
+
<div class="grid gap-6 xl:grid-cols-[20rem_minmax(0,1fr)]"><ProfileCard {...profileData} {variant} /><div class="space-y-4"><div class="flex items-center justify-between border-b border-border pb-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('publicProfile.activity')}</h2><Badge variant="outline">{activities.length}</Badge></div><ActivityTimeline activities={activities} /></div></div>
|
|
40
46
|
{:else}
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
<Tabs.Trigger value="projects">{i18n.t('publicProfile.projects')}</Tabs.Trigger>
|
|
44
|
-
<Tabs.Trigger value="activity">{i18n.t('publicProfile.activity')}</Tabs.Trigger>
|
|
45
|
-
<Tabs.Trigger value="teams">{i18n.t('publicProfile.teams')}</Tabs.Trigger>
|
|
46
|
-
</Tabs.List>
|
|
47
|
-
<Tabs.Content value="projects" class="mt-5"><ProjectsGrid {projects} {columns} /></Tabs.Content>
|
|
48
|
-
<Tabs.Content value="activity" class="mt-5"><ActivityTimeline activities={activities} /></Tabs.Content>
|
|
49
|
-
<Tabs.Content value="teams" class="mt-5"><TeamsShowcase {teams} /></Tabs.Content>
|
|
50
|
-
</Tabs.Root>
|
|
47
|
+
<div class="grid gap-4 sm:grid-cols-3">{#each profileData.stats as stat (stat.label)}<div class="border-l-2 border-primary pl-3"><p class="text-sm text-muted-foreground">{stat.label}</p><p class="mt-1 text-xl font-semibold text-foreground">{stat.value}</p></div>{/each}</div>
|
|
48
|
+
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]"><TeamsShowcase {teams} /><ProfileCard {...profileData} {variant} /></div>
|
|
51
49
|
{/if}
|
|
52
50
|
</ContentPageShell>
|
package/dist/index.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export { default as InlineEdit } from "./components/InlineEdit.svelte";
|
|
|
63
63
|
export { default as VirtualTable } from "./components/VirtualTable.svelte";
|
|
64
64
|
export { default as DraggableHeader } from "./components/DraggableHeader.svelte";
|
|
65
65
|
export { default as SettingsPage } from "./components/SettingsPage.svelte";
|
|
66
|
+
export { default as FeedbackNotice } from './components/content/FeedbackNotice.svelte';
|
|
67
|
+
export type { FeedbackNoticeTone, FeedbackNoticePriority, } from './components/content/FeedbackNotice.svelte';
|
|
66
68
|
export { setRichTextEditor, getRichTextEditor } from "./editor-config.svelte.js";
|
|
67
69
|
export { default as AppearanceSettings } from "./components/AppearanceSettings.svelte";
|
|
68
70
|
export { default as AboutSettings } from "./components/AboutSettings.svelte";
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ export { default as InlineEdit } from "./components/InlineEdit.svelte";
|
|
|
66
66
|
export { default as VirtualTable } from "./components/VirtualTable.svelte";
|
|
67
67
|
export { default as DraggableHeader } from "./components/DraggableHeader.svelte";
|
|
68
68
|
export { default as SettingsPage } from "./components/SettingsPage.svelte";
|
|
69
|
+
export { default as FeedbackNotice } from './components/content/FeedbackNotice.svelte';
|
|
69
70
|
// Editor Registry
|
|
70
71
|
export { setRichTextEditor, getRichTextEditor } from "./editor-config.svelte.js";
|
|
71
72
|
// Settings & Preferences
|
package/dist/reference-data.js
CHANGED
|
@@ -12,6 +12,14 @@ export const referenceDemoData = {
|
|
|
12
12
|
{ id: '2', name: 'API Gateway v2', description: 'High-performance API gateway with rate limiting and circuit breaker patterns.', status: 'active', members: 5, tasks: 21, progress: 45, tags: ['Backend', 'Infra'] },
|
|
13
13
|
{ id: '3', name: 'Mobile App', description: 'Cross-platform mobile application built with shared native modules.', status: 'completed', members: 12, tasks: 56, progress: 100, tags: ['Mobile', 'Product'] },
|
|
14
14
|
{ id: '4', name: 'Data Pipeline', description: 'Real-time data processing pipeline for event streaming.', status: 'on-hold', members: 3, tasks: 18, progress: 20, tags: ['Data', 'Streaming'] },
|
|
15
|
+
{ id: '5', name: 'Access Review', description: 'Quarterly access certification and role ownership workflow.', status: 'active', members: 4, tasks: 16, progress: 63, tags: ['Security', 'RBAC'] },
|
|
16
|
+
{ id: '6', name: 'Billing Portal', description: 'Self-service plans, invoices, and subscription controls.', status: 'active', members: 6, tasks: 27, progress: 58, tags: ['Billing', 'Portal'] },
|
|
17
|
+
{ id: '7', name: 'Warehouse Console', description: 'Receiving, transfers, cycle counts, and exception handling.', status: 'completed', members: 9, tasks: 48, progress: 100, tags: ['Inventory', 'Operations'] },
|
|
18
|
+
{ id: '8', name: 'Notification Center', description: 'Priority routing, delivery preferences, and read-state controls.', status: 'active', members: 5, tasks: 22, progress: 74, tags: ['Messaging', 'Workflow'] },
|
|
19
|
+
{ id: '9', name: 'Partner Directory', description: 'Supplier, customer, and integration partner records.', status: 'active', members: 7, tasks: 31, progress: 46, tags: ['Directory', 'CRM'] },
|
|
20
|
+
{ id: '10', name: 'Audit Export', description: 'Scheduled evidence packages for security and compliance review.', status: 'on-hold', members: 3, tasks: 14, progress: 28, tags: ['Audit', 'Export'] },
|
|
21
|
+
{ id: '11', name: 'Team Onboarding', description: 'Invitations, import validation, and first-week account setup.', status: 'active', members: 5, tasks: 19, progress: 81, tags: ['People', 'Onboarding'] },
|
|
22
|
+
{ id: '12', name: 'AI Prompt Library', description: 'Governed operational prompts with ownership and usage signals.', status: 'completed', members: 4, tasks: 24, progress: 100, tags: ['AI', 'Governance'] },
|
|
15
23
|
],
|
|
16
24
|
teams: [
|
|
17
25
|
{ id: '1', name: 'Frontend Platform', description: 'Core UI component library and design system.', totalMembers: 8, members: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Carol' }] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./components/*": "./dist/components/*"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@svadmin/core": "^0.
|
|
42
|
+
"@svadmin/core": "^0.38.0",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|