@svadmin/ui 0.3.0 → 0.5.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/package.json +9 -4
- package/src/actions.ts +110 -0
- package/src/components/AdminApp.svelte +7 -4
- package/src/components/Authenticated.svelte +1 -7
- package/src/components/AutoForm.svelte +158 -69
- package/src/components/AutoSaveIndicator.svelte +19 -14
- package/src/components/AutoTable.svelte +465 -243
- package/src/components/Breadcrumbs.svelte +20 -17
- package/src/components/CanAccess.svelte +6 -4
- package/src/components/ComboboxField.svelte +122 -0
- package/src/components/CommandPalette.svelte +93 -0
- package/src/components/ConfigErrorScreen.svelte +53 -151
- package/src/components/ConfirmDialog.svelte +25 -17
- package/src/components/CreatePage.svelte +3 -2
- package/src/components/DevTools.svelte +99 -230
- package/src/components/DrawerForm.svelte +19 -10
- package/src/components/EditPage.svelte +25 -6
- package/src/components/EmptyState.svelte +5 -1
- package/src/components/ErrorBoundary.svelte +38 -40
- package/src/components/ErrorComponent.svelte +18 -17
- package/src/components/FieldRenderer.svelte +240 -75
- package/src/components/ForgotPasswordPage.svelte +20 -111
- package/src/components/Header.svelte +28 -8
- package/src/components/InferencerPanel.svelte +136 -110
- package/src/components/InfiniteList.svelte +93 -0
- package/src/components/Layout.svelte +85 -22
- package/src/components/ListPage.svelte +25 -1
- package/src/components/LiveIndicator.svelte +30 -87
- package/src/components/LoginPage.svelte +81 -155
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/PageSkeleton.svelte +2 -1
- package/src/components/PasswordInput.svelte +95 -0
- package/src/components/ProfilePage.svelte +166 -0
- package/src/components/RegisterPage.svelte +35 -164
- package/src/components/ShowPage.svelte +32 -33
- package/src/components/Sidebar.svelte +185 -73
- package/src/components/StatsCard.svelte +12 -3
- package/src/components/StepsForm.svelte +139 -0
- package/src/components/Toast.svelte +15 -54
- package/src/components/TooltipButton.svelte +52 -0
- package/src/components/UndoableNotification.svelte +28 -96
- package/src/components/UpdatePasswordPage.svelte +32 -160
- package/src/components/buttons/RefreshButton.svelte +7 -2
- package/src/components/fieldComponentMap.ts +79 -0
- package/src/components/fields/BooleanField.svelte +9 -9
- package/src/components/fields/EmailField.svelte +2 -8
- package/src/components/fields/FileField.svelte +4 -9
- package/src/components/fields/ImageField.svelte +2 -7
- package/src/components/fields/JsonField.svelte +72 -0
- package/src/components/fields/MarkdownField.svelte +2 -12
- package/src/components/fields/MultiSelectField.svelte +20 -0
- package/src/components/fields/RelationField.svelte +24 -0
- package/src/components/fields/RichTextField.svelte +35 -0
- package/src/components/fields/SelectField.svelte +14 -0
- package/src/components/fields/TagField.svelte +6 -11
- package/src/components/fields/TextField.svelte +2 -7
- package/src/components/fields/UrlField.svelte +4 -10
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
- package/src/components/ui/alert-dialog/index.ts +20 -0
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
- package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/collapsible/index.ts +7 -0
- package/src/components/ui/command/command-dialog.svelte +31 -0
- package/src/components/ui/command/index.ts +2 -0
- package/src/components/ui/context-menu/index.ts +11 -0
- package/src/components/ui/drawer/drawer-content.svelte +21 -0
- package/src/components/ui/drawer/drawer-description.svelte +11 -0
- package/src/components/ui/drawer/drawer-footer.svelte +11 -0
- package/src/components/ui/drawer/drawer-header.svelte +11 -0
- package/src/components/ui/drawer/drawer-title.svelte +11 -0
- package/src/components/ui/drawer/drawer.svelte +18 -0
- package/src/components/ui/drawer/index.ts +6 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
- package/src/components/ui/dropdown-menu/index.ts +6 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/label/label.svelte +18 -0
- package/src/components/ui/pagination/index.ts +7 -0
- package/src/components/ui/pagination/pagination-content.svelte +11 -0
- package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
- package/src/components/ui/pagination/pagination-item.svelte +11 -0
- package/src/components/ui/pagination/pagination-link.svelte +26 -0
- package/src/components/ui/pagination/pagination-next.svelte +22 -0
- package/src/components/ui/pagination/pagination-previous.svelte +22 -0
- package/src/components/ui/pagination/pagination.svelte +11 -0
- package/src/components/ui/popover/index.ts +12 -0
- package/src/components/ui/popover/popover-content.svelte +25 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/progress/progress.svelte +22 -0
- package/src/components/ui/scroll-area/index.ts +1 -0
- package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
- package/src/components/ui/sheet/index.ts +15 -1
- package/src/components/ui/sheet/sheet-content.svelte +28 -0
- package/src/components/ui/sheet/sheet-description.svelte +22 -0
- package/src/components/ui/sheet/sheet-footer.svelte +22 -0
- package/src/components/ui/sheet/sheet-header.svelte +22 -0
- package/src/components/ui/sheet/sheet-title.svelte +22 -0
- package/src/index.ts +31 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useGetIdentity, useUpdatePassword } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Label } from './ui/label/index.js';
|
|
6
|
+
import * as Card from './ui/card/index.js';
|
|
7
|
+
import * as Alert from './ui/alert/index.js';
|
|
8
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
9
|
+
import { User, Mail, Lock, Loader2, AlertCircle, CheckCircle } from 'lucide-svelte';
|
|
10
|
+
|
|
11
|
+
const identity = useGetIdentity();
|
|
12
|
+
const updatePw = useUpdatePassword();
|
|
13
|
+
|
|
14
|
+
// Password change form
|
|
15
|
+
let currentPassword = $state('');
|
|
16
|
+
let newPassword = $state('');
|
|
17
|
+
let confirmPassword = $state('');
|
|
18
|
+
let pwError = $state('');
|
|
19
|
+
let pwSuccess = $state(false);
|
|
20
|
+
|
|
21
|
+
async function handlePasswordChange(e: SubmitEvent) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
pwError = '';
|
|
24
|
+
pwSuccess = false;
|
|
25
|
+
|
|
26
|
+
if (!newPassword) { pwError = t('auth.passwordRequired'); return; }
|
|
27
|
+
if (newPassword !== confirmPassword) { pwError = t('auth.passwordMismatch'); return; }
|
|
28
|
+
|
|
29
|
+
const result = await updatePw.mutate({
|
|
30
|
+
password: newPassword,
|
|
31
|
+
currentPassword,
|
|
32
|
+
confirmPassword,
|
|
33
|
+
});
|
|
34
|
+
if (result.success) {
|
|
35
|
+
pwSuccess = true;
|
|
36
|
+
currentPassword = '';
|
|
37
|
+
newPassword = '';
|
|
38
|
+
confirmPassword = '';
|
|
39
|
+
} else {
|
|
40
|
+
pwError = result.error?.message ?? t('common.operationFailed');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Avatar initials
|
|
45
|
+
const initials = $derived(
|
|
46
|
+
identity.data?.name
|
|
47
|
+
? identity.data.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
|
|
48
|
+
: '?'
|
|
49
|
+
);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="max-w-2xl mx-auto space-y-6 p-4 sm:p-6">
|
|
53
|
+
<!-- Profile Info -->
|
|
54
|
+
<Card.Card>
|
|
55
|
+
<Card.CardHeader>
|
|
56
|
+
<Card.CardTitle class="flex items-center gap-2">
|
|
57
|
+
<User class="h-5 w-5" />
|
|
58
|
+
{t('profile.title')}
|
|
59
|
+
</Card.CardTitle>
|
|
60
|
+
</Card.CardHeader>
|
|
61
|
+
<Card.CardContent>
|
|
62
|
+
{#if identity.isLoading}
|
|
63
|
+
<div class="flex items-center gap-2 text-muted-foreground">
|
|
64
|
+
<Loader2 class="h-4 w-4 animate-spin" />
|
|
65
|
+
{t('common.loading')}
|
|
66
|
+
</div>
|
|
67
|
+
{:else if identity.data}
|
|
68
|
+
<div class="flex items-start gap-6">
|
|
69
|
+
<!-- Avatar -->
|
|
70
|
+
<div class="shrink-0">
|
|
71
|
+
{#if identity.data.avatar}
|
|
72
|
+
<img
|
|
73
|
+
src={identity.data.avatar}
|
|
74
|
+
alt={identity.data.name ?? ''}
|
|
75
|
+
class="h-20 w-20 rounded-xl object-cover ring-2 ring-primary/20"
|
|
76
|
+
/>
|
|
77
|
+
{:else}
|
|
78
|
+
<div class="h-20 w-20 rounded-xl bg-primary/10 text-primary flex items-center justify-center text-2xl font-bold">
|
|
79
|
+
{initials}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Info -->
|
|
85
|
+
<div class="space-y-3 flex-1 min-w-0">
|
|
86
|
+
<div>
|
|
87
|
+
<Label class="text-xs text-muted-foreground">{t('profile.name')}</Label>
|
|
88
|
+
<p class="text-lg font-semibold truncate">{identity.data.name ?? '—'}</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{#if identity.data.email}
|
|
92
|
+
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
93
|
+
<Mail class="h-4 w-4 shrink-0" />
|
|
94
|
+
<span class="truncate">{identity.data.email}</span>
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
97
|
+
|
|
98
|
+
{#if identity.data.id}
|
|
99
|
+
<div class="text-xs text-muted-foreground/60 font-mono">
|
|
100
|
+
ID: {identity.data.id}
|
|
101
|
+
</div>
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
{:else}
|
|
106
|
+
<p class="text-sm text-muted-foreground">{t('profile.notAvailable')}</p>
|
|
107
|
+
{/if}
|
|
108
|
+
</Card.CardContent>
|
|
109
|
+
</Card.Card>
|
|
110
|
+
|
|
111
|
+
<!-- Change Password -->
|
|
112
|
+
<Card.Card>
|
|
113
|
+
<Card.CardHeader>
|
|
114
|
+
<Card.CardTitle class="flex items-center gap-2">
|
|
115
|
+
<Lock class="h-5 w-5" />
|
|
116
|
+
{t('profile.changePassword')}
|
|
117
|
+
</Card.CardTitle>
|
|
118
|
+
</Card.CardHeader>
|
|
119
|
+
<Card.CardContent>
|
|
120
|
+
<form onsubmit={handlePasswordChange} class="space-y-4 max-w-sm">
|
|
121
|
+
{#if pwError}
|
|
122
|
+
<Alert.Root variant="destructive">
|
|
123
|
+
<AlertCircle class="h-4 w-4" />
|
|
124
|
+
<Alert.Description>{pwError}</Alert.Description>
|
|
125
|
+
</Alert.Root>
|
|
126
|
+
{/if}
|
|
127
|
+
|
|
128
|
+
{#if pwSuccess}
|
|
129
|
+
<Alert.Root class="border-green-500/30 bg-green-500/5 text-green-600 dark:text-green-400">
|
|
130
|
+
<CheckCircle class="h-4 w-4" />
|
|
131
|
+
<Alert.Description>{t('profile.passwordChanged')}</Alert.Description>
|
|
132
|
+
</Alert.Root>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
<PasswordInput
|
|
136
|
+
id="current-password"
|
|
137
|
+
label={t('profile.currentPassword')}
|
|
138
|
+
bind:value={currentPassword}
|
|
139
|
+
autocomplete="current-password"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<PasswordInput
|
|
143
|
+
id="new-password"
|
|
144
|
+
label={t('auth.password')}
|
|
145
|
+
bind:value={newPassword}
|
|
146
|
+
autocomplete="new-password"
|
|
147
|
+
showStrength
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<PasswordInput
|
|
151
|
+
id="confirm-new-password"
|
|
152
|
+
label={t('auth.confirmPassword')}
|
|
153
|
+
bind:value={confirmPassword}
|
|
154
|
+
autocomplete="new-password"
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
<Button type="submit" disabled={updatePw.isLoading}>
|
|
158
|
+
{#if updatePw.isLoading}
|
|
159
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
160
|
+
{/if}
|
|
161
|
+
{t('profile.updatePassword')}
|
|
162
|
+
</Button>
|
|
163
|
+
</form>
|
|
164
|
+
</Card.CardContent>
|
|
165
|
+
</Card.Card>
|
|
166
|
+
</div>
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
import { navigate } from '@svadmin/core/router';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
7
|
+
import { Label } from './ui/label/index.js';
|
|
7
8
|
import * as Card from './ui/card/index.js';
|
|
8
|
-
import
|
|
9
|
+
import * as Alert from './ui/alert/index.js';
|
|
10
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
import { UserPlus, Mail, Loader2, AlertCircle } from 'lucide-svelte';
|
|
9
12
|
|
|
10
13
|
let { title = 'Admin', onSuccess } = $props<{
|
|
11
14
|
title?: string;
|
|
@@ -17,7 +20,6 @@
|
|
|
17
20
|
let email = $state('');
|
|
18
21
|
let password = $state('');
|
|
19
22
|
let confirmPassword = $state('');
|
|
20
|
-
let showPassword = $state(false);
|
|
21
23
|
let error = $state('');
|
|
22
24
|
|
|
23
25
|
async function handleSubmit(e: SubmitEvent) {
|
|
@@ -37,11 +39,11 @@
|
|
|
37
39
|
}
|
|
38
40
|
</script>
|
|
39
41
|
|
|
40
|
-
<div class="
|
|
41
|
-
<div class="
|
|
42
|
-
<Card.Card class="
|
|
43
|
-
<Card.CardHeader class="
|
|
44
|
-
<div class="
|
|
42
|
+
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/[0.08] via-background to-primary/[0.04] p-4">
|
|
43
|
+
<div class="w-full max-w-[420px]">
|
|
44
|
+
<Card.Card class="backdrop-blur-xl border-border/50 shadow-[0_8px_32px_hsl(var(--primary)/0.08),0_2px_8px_hsl(0_0%_0%/0.06)]">
|
|
45
|
+
<Card.CardHeader class="text-center pb-2">
|
|
46
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mx-auto mb-3">
|
|
45
47
|
<UserPlus class="h-6 w-6" />
|
|
46
48
|
</div>
|
|
47
49
|
<Card.CardTitle class="text-2xl font-bold">{t('auth.createAccount')}</Card.CardTitle>
|
|
@@ -50,15 +52,16 @@
|
|
|
50
52
|
<Card.CardContent>
|
|
51
53
|
<form onsubmit={handleSubmit} class="space-y-4">
|
|
52
54
|
{#if error}
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
55
|
+
<Alert.Root variant="destructive">
|
|
56
|
+
<AlertCircle class="h-4 w-4" />
|
|
57
|
+
<Alert.Description>{error}</Alert.Description>
|
|
58
|
+
</Alert.Root>
|
|
56
59
|
{/if}
|
|
57
60
|
|
|
58
61
|
<div class="space-y-2">
|
|
59
|
-
<
|
|
60
|
-
<div class="
|
|
61
|
-
<Mail class="
|
|
62
|
+
<Label for="register-email">{t('auth.email')}</Label>
|
|
63
|
+
<div class="relative">
|
|
64
|
+
<Mail class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
62
65
|
<Input
|
|
63
66
|
id="register-email"
|
|
64
67
|
type="email"
|
|
@@ -70,63 +73,34 @@
|
|
|
70
73
|
</div>
|
|
71
74
|
</div>
|
|
72
75
|
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class="password-toggle"
|
|
88
|
-
onclick={() => showPassword = !showPassword}
|
|
89
|
-
tabindex={-1}
|
|
90
|
-
>
|
|
91
|
-
{#if showPassword}
|
|
92
|
-
<EyeOff class="h-4 w-4" />
|
|
93
|
-
{:else}
|
|
94
|
-
<Eye class="h-4 w-4" />
|
|
95
|
-
{/if}
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<div class="space-y-2">
|
|
101
|
-
<label for="register-confirm" class="text-sm font-medium text-foreground">{t('auth.confirmPassword')}</label>
|
|
102
|
-
<div class="input-with-icon">
|
|
103
|
-
<Lock class="input-icon h-4 w-4" />
|
|
104
|
-
<Input
|
|
105
|
-
id="register-confirm"
|
|
106
|
-
type={showPassword ? 'text' : 'password'}
|
|
107
|
-
placeholder="••••••••"
|
|
108
|
-
bind:value={confirmPassword}
|
|
109
|
-
class="pl-9"
|
|
110
|
-
autocomplete="new-password"
|
|
111
|
-
/>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
76
|
+
<PasswordInput
|
|
77
|
+
id="register-password"
|
|
78
|
+
label={t('auth.password')}
|
|
79
|
+
bind:value={password}
|
|
80
|
+
autocomplete="new-password"
|
|
81
|
+
showStrength
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<PasswordInput
|
|
85
|
+
id="register-confirm"
|
|
86
|
+
label={t('auth.confirmPassword')}
|
|
87
|
+
bind:value={confirmPassword}
|
|
88
|
+
autocomplete="new-password"
|
|
89
|
+
/>
|
|
114
90
|
|
|
115
91
|
<Button type="submit" class="w-full" disabled={register.isLoading}>
|
|
116
92
|
{#if register.isLoading}
|
|
117
|
-
<
|
|
93
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
118
94
|
{/if}
|
|
119
95
|
{t('auth.registerButton')}
|
|
120
96
|
</Button>
|
|
121
97
|
</form>
|
|
122
98
|
|
|
123
|
-
<div class="
|
|
99
|
+
<div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
|
|
124
100
|
<span class="text-sm text-muted-foreground">{t('auth.hasAccount')}</span>
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
onclick={() => navigate('/login')}
|
|
129
|
-
>{t('auth.login')}</button>
|
|
101
|
+
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
|
|
102
|
+
{t('auth.login')}
|
|
103
|
+
</Button>
|
|
130
104
|
</div>
|
|
131
105
|
</Card.CardContent>
|
|
132
106
|
</Card.Card>
|
|
@@ -136,106 +110,3 @@
|
|
|
136
110
|
</p>
|
|
137
111
|
</div>
|
|
138
112
|
</div>
|
|
139
|
-
|
|
140
|
-
<style>
|
|
141
|
-
.register-page {
|
|
142
|
-
min-height: 100vh;
|
|
143
|
-
display: flex;
|
|
144
|
-
align-items: center;
|
|
145
|
-
justify-content: center;
|
|
146
|
-
background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
|
|
147
|
-
padding: 1rem;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.register-container {
|
|
151
|
-
width: 100%;
|
|
152
|
-
max-width: 420px;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.register-icon {
|
|
156
|
-
display: inline-flex;
|
|
157
|
-
align-items: center;
|
|
158
|
-
justify-content: center;
|
|
159
|
-
width: 48px;
|
|
160
|
-
height: 48px;
|
|
161
|
-
border-radius: 12px;
|
|
162
|
-
background: hsl(var(--primary) / 0.1);
|
|
163
|
-
color: hsl(var(--primary));
|
|
164
|
-
margin: 0 auto 0.75rem;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
:global(.login-card) {
|
|
168
|
-
backdrop-filter: blur(20px);
|
|
169
|
-
border: 1px solid hsl(var(--border) / 0.5);
|
|
170
|
-
box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
:global(.login-header) {
|
|
174
|
-
text-align: center;
|
|
175
|
-
padding-bottom: 0.5rem;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.input-with-icon {
|
|
179
|
-
position: relative;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
:global(.input-icon) {
|
|
183
|
-
position: absolute;
|
|
184
|
-
left: 0.75rem;
|
|
185
|
-
top: 50%;
|
|
186
|
-
transform: translateY(-50%);
|
|
187
|
-
color: hsl(var(--muted-foreground));
|
|
188
|
-
pointer-events: none;
|
|
189
|
-
z-index: 1;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.password-toggle {
|
|
193
|
-
position: absolute;
|
|
194
|
-
right: 0.75rem;
|
|
195
|
-
top: 50%;
|
|
196
|
-
transform: translateY(-50%);
|
|
197
|
-
color: hsl(var(--muted-foreground));
|
|
198
|
-
background: none;
|
|
199
|
-
border: none;
|
|
200
|
-
cursor: pointer;
|
|
201
|
-
padding: 2px;
|
|
202
|
-
z-index: 1;
|
|
203
|
-
}
|
|
204
|
-
.password-toggle:hover {
|
|
205
|
-
color: hsl(var(--foreground));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.error-alert {
|
|
209
|
-
padding: 0.75rem;
|
|
210
|
-
border-radius: 0.5rem;
|
|
211
|
-
background: hsl(var(--destructive) / 0.1);
|
|
212
|
-
border: 1px solid hsl(var(--destructive) / 0.3);
|
|
213
|
-
color: hsl(var(--destructive));
|
|
214
|
-
font-size: 0.875rem;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.loading-spinner {
|
|
218
|
-
display: inline-block;
|
|
219
|
-
width: 16px;
|
|
220
|
-
height: 16px;
|
|
221
|
-
border: 2px solid transparent;
|
|
222
|
-
border-top-color: currentColor;
|
|
223
|
-
border-radius: 50%;
|
|
224
|
-
animation: spin 0.6s linear infinite;
|
|
225
|
-
margin-right: 0.5rem;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
@keyframes spin {
|
|
229
|
-
to { transform: rotate(360deg); }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.auth-link {
|
|
233
|
-
display: flex;
|
|
234
|
-
align-items: center;
|
|
235
|
-
justify-content: center;
|
|
236
|
-
gap: 0.25rem;
|
|
237
|
-
margin-top: 1.25rem;
|
|
238
|
-
padding-top: 1.25rem;
|
|
239
|
-
border-top: 1px solid hsl(var(--border));
|
|
240
|
-
}
|
|
241
|
-
</style>
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
import { t, getLocale } from '@svadmin/core/i18n';
|
|
6
6
|
import { Button } from './ui/button/index.js';
|
|
7
7
|
import * as Card from './ui/card/index.js';
|
|
8
|
-
import {
|
|
8
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
9
|
+
import PageHeader from './PageHeader.svelte';
|
|
10
|
+
import { getDisplayComponent } from './fieldComponentMap';
|
|
11
|
+
import { Pencil } from 'lucide-svelte';
|
|
9
12
|
|
|
10
13
|
let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
|
|
11
14
|
|
|
@@ -13,54 +16,50 @@
|
|
|
13
16
|
const showFields = resource.fields.filter(f => f.showInShow !== false);
|
|
14
17
|
|
|
15
18
|
const query = useShow({ resource: resourceName, id });
|
|
16
|
-
|
|
17
|
-
function formatValue(field: FieldDefinition, value: unknown): string {
|
|
18
|
-
if (value == null) return '—';
|
|
19
|
-
if (field.type === 'boolean') return value ? t('common.yes') : t('common.no');
|
|
20
|
-
if (field.type === 'date') return new Date(value as string).toLocaleDateString(getLocale());
|
|
21
|
-
if (field.type === 'json') return JSON.stringify(value, null, 2);
|
|
22
|
-
if (field.type === 'tags' && Array.isArray(value)) return (value as string[]).join(', ');
|
|
23
|
-
if (field.type === 'select' && field.options) {
|
|
24
|
-
const opt = field.options.find(o => o.value === value);
|
|
25
|
-
return opt?.label ?? String(value);
|
|
26
|
-
}
|
|
27
|
-
return String(value);
|
|
28
|
-
}
|
|
29
19
|
</script>
|
|
30
20
|
|
|
31
21
|
<div class="space-y-6">
|
|
32
|
-
<
|
|
33
|
-
|
|
22
|
+
<PageHeader title="{resource.label} {t('common.detail')}">
|
|
23
|
+
{#snippet actions()}
|
|
34
24
|
<Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
|
|
35
|
-
<
|
|
36
|
-
</Button>
|
|
37
|
-
<h1 class="text-2xl font-bold text-foreground">{resource.label} {t('common.detail')}</h1>
|
|
38
|
-
</div>
|
|
39
|
-
{#if resource.canEdit !== false}
|
|
40
|
-
<Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
|
|
41
|
-
<Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
|
|
25
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
|
42
26
|
</Button>
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
{#if resource.canEdit !== false}
|
|
28
|
+
<Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
|
|
29
|
+
<Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
|
|
30
|
+
</Button>
|
|
31
|
+
{/if}
|
|
32
|
+
{/snippet}
|
|
33
|
+
</PageHeader>
|
|
45
34
|
|
|
46
35
|
{#if query.query.isLoading}
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
36
|
+
<Card.Root>
|
|
37
|
+
<Card.Content class="divide-y divide-border p-0">
|
|
38
|
+
{#each showFields.slice(0, 6) as _}
|
|
39
|
+
<div class="flex px-6 py-4">
|
|
40
|
+
<Skeleton class="h-4 w-1/4" />
|
|
41
|
+
<Skeleton class="h-4 w-2/5 ml-auto" />
|
|
42
|
+
</div>
|
|
43
|
+
{/each}
|
|
44
|
+
</Card.Content>
|
|
45
|
+
</Card.Root>
|
|
50
46
|
{:else if query.query.data?.data}
|
|
51
47
|
<Card.Root>
|
|
52
48
|
<Card.Content class="divide-y divide-border p-0">
|
|
53
49
|
{#each showFields as field}
|
|
54
50
|
{@const value = (query.query.data!.data as Record<string, unknown>)[field.key]}
|
|
51
|
+
{@const DisplayComponent = getDisplayComponent(field.type)}
|
|
55
52
|
<div class="flex px-6 py-4">
|
|
56
53
|
<div class="w-1/3 text-sm font-medium text-muted-foreground">{field.label}</div>
|
|
57
54
|
<div class="w-2/3 text-sm text-foreground">
|
|
58
|
-
{#if
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
{#if DisplayComponent && value != null}
|
|
56
|
+
<DisplayComponent
|
|
57
|
+
{value}
|
|
58
|
+
options={field.options}
|
|
59
|
+
resourceName={field.resource}
|
|
60
|
+
/>
|
|
62
61
|
{:else}
|
|
63
|
-
{
|
|
62
|
+
{value != null ? String(value) : '—'}
|
|
64
63
|
{/if}
|
|
65
64
|
</div>
|
|
66
65
|
</div>
|