@svadmin/ui 0.3.0 → 0.4.1
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 +7 -3
- 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/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 +33 -154
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/PageSkeleton.svelte +2 -1
- package/src/components/PasswordInput.svelte +95 -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 +30 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
2
3
|
import { Wifi, WifiOff, Loader2 } from 'lucide-svelte';
|
|
4
|
+
import { Badge } from './ui/badge/index.js';
|
|
3
5
|
|
|
4
6
|
let {
|
|
5
7
|
status = 'disconnected',
|
|
@@ -14,12 +16,12 @@
|
|
|
14
16
|
}>();
|
|
15
17
|
|
|
16
18
|
const statusConfigs = {
|
|
17
|
-
connected: {
|
|
18
|
-
connecting: {
|
|
19
|
-
disconnected: {
|
|
19
|
+
connected: { variant: 'default' as const, dotClass: 'bg-emerald-500', label: 'Live', icon: Wifi },
|
|
20
|
+
connecting: { variant: 'secondary' as const, dotClass: 'bg-amber-500', label: 'Connecting...', icon: Loader2 as typeof Wifi },
|
|
21
|
+
disconnected: { variant: 'destructive' as const, dotClass: 'bg-red-500', label: 'Offline', icon: WifiOff },
|
|
20
22
|
} as const;
|
|
21
23
|
type StatusKey = 'connected' | 'connecting' | 'disconnected';
|
|
22
|
-
const
|
|
24
|
+
const cfg = $derived(statusConfigs[status as StatusKey]);
|
|
23
25
|
|
|
24
26
|
const timeSinceEvent = $derived(
|
|
25
27
|
lastEvent?.timestamp
|
|
@@ -28,88 +30,29 @@
|
|
|
28
30
|
);
|
|
29
31
|
</script>
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
{
|
|
34
|
-
<span class="
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{#if showDetails && lastEvent}
|
|
46
|
-
<span class="live-event">
|
|
47
|
-
{lastEvent.type}
|
|
48
|
-
{#if timeSinceEvent !== null && timeSinceEvent < 60}
|
|
49
|
-
<span class="live-time">{timeSinceEvent}s ago</span>
|
|
33
|
+
{#key status}
|
|
34
|
+
<div transition:fade={{ duration: 200 }}>
|
|
35
|
+
<Badge variant={cfg.variant} class="gap-1.5 select-none" title={cfg.label}>
|
|
36
|
+
<span class="relative flex h-2 w-2">
|
|
37
|
+
<span class="{cfg.dotClass} rounded-full h-2 w-2"></span>
|
|
38
|
+
{#if status === 'connected'}
|
|
39
|
+
<span class="absolute inset-0 rounded-full {cfg.dotClass} opacity-40 animate-ping"></span>
|
|
40
|
+
{/if}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="text-[0.6875rem] font-semibold uppercase tracking-wide">{cfg.label}</span>
|
|
43
|
+
|
|
44
|
+
{#if showDetails && resource}
|
|
45
|
+
<span class="text-[0.625rem] font-mono opacity-70">{resource}</span>
|
|
50
46
|
{/if}
|
|
51
|
-
</span>
|
|
52
|
-
{/if}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<style>
|
|
56
|
-
.live-indicator {
|
|
57
|
-
display: inline-flex;
|
|
58
|
-
align-items: center;
|
|
59
|
-
gap: 0.375rem;
|
|
60
|
-
font-size: 0.75rem;
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
padding: 0.25rem 0.5rem;
|
|
63
|
-
border-radius: 9999px;
|
|
64
|
-
background: hsl(var(--muted) / 0.3);
|
|
65
|
-
user-select: none;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.live-dot {
|
|
69
|
-
position: relative;
|
|
70
|
-
width: 8px;
|
|
71
|
-
height: 8px;
|
|
72
|
-
border-radius: 50%;
|
|
73
|
-
background: var(--dot-color);
|
|
74
|
-
flex-shrink: 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.live-pulse {
|
|
78
|
-
position: absolute;
|
|
79
|
-
inset: -3px;
|
|
80
|
-
border-radius: 50%;
|
|
81
|
-
background: var(--dot-color);
|
|
82
|
-
opacity: 0.4;
|
|
83
|
-
animation: live-pulse 2s ease-in-out infinite;
|
|
84
|
-
}
|
|
85
47
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.live-resource {
|
|
99
|
-
font-size: 0.625rem;
|
|
100
|
-
color: hsl(var(--muted-foreground));
|
|
101
|
-
font-family: monospace;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.live-event {
|
|
105
|
-
font-size: 0.625rem;
|
|
106
|
-
color: hsl(var(--muted-foreground));
|
|
107
|
-
display: flex;
|
|
108
|
-
align-items: center;
|
|
109
|
-
gap: 0.25rem;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.live-time {
|
|
113
|
-
opacity: 0.6;
|
|
114
|
-
}
|
|
115
|
-
</style>
|
|
48
|
+
{#if showDetails && lastEvent}
|
|
49
|
+
<span class="text-[0.625rem] opacity-70">
|
|
50
|
+
{lastEvent.type}
|
|
51
|
+
{#if timeSinceEvent !== null && timeSinceEvent < 60}
|
|
52
|
+
<span class="opacity-60">{timeSinceEvent}s ago</span>
|
|
53
|
+
{/if}
|
|
54
|
+
</span>
|
|
55
|
+
{/if}
|
|
56
|
+
</Badge>
|
|
57
|
+
</div>
|
|
58
|
+
{/key}
|
|
@@ -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 { LogIn, 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
|
|
|
18
21
|
let email = $state('');
|
|
19
22
|
let password = $state('');
|
|
20
|
-
let showPassword = $state(false);
|
|
21
23
|
let error = $state('');
|
|
22
24
|
|
|
23
25
|
async function handleSubmit(e: SubmitEvent) {
|
|
@@ -36,11 +38,11 @@
|
|
|
36
38
|
}
|
|
37
39
|
</script>
|
|
38
40
|
|
|
39
|
-
<div class="
|
|
40
|
-
<div class="
|
|
41
|
-
<Card.Card class="
|
|
42
|
-
<Card.CardHeader class="
|
|
43
|
-
<div class="
|
|
41
|
+
<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">
|
|
42
|
+
<div class="w-full max-w-[420px]">
|
|
43
|
+
<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)]">
|
|
44
|
+
<Card.CardHeader class="text-center pb-2">
|
|
45
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mx-auto mb-3">
|
|
44
46
|
<LogIn class="h-6 w-6" />
|
|
45
47
|
</div>
|
|
46
48
|
<Card.CardTitle class="text-2xl font-bold">{t('auth.welcomeBack')}</Card.CardTitle>
|
|
@@ -49,15 +51,16 @@
|
|
|
49
51
|
<Card.CardContent>
|
|
50
52
|
<form onsubmit={handleSubmit} class="space-y-4">
|
|
51
53
|
{#if error}
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
54
|
+
<Alert.Root variant="destructive">
|
|
55
|
+
<AlertCircle class="h-4 w-4" />
|
|
56
|
+
<Alert.Description>{error}</Alert.Description>
|
|
57
|
+
</Alert.Root>
|
|
55
58
|
{/if}
|
|
56
59
|
|
|
57
60
|
<div class="space-y-2">
|
|
58
|
-
<
|
|
59
|
-
<div class="
|
|
60
|
-
<Mail class="
|
|
61
|
+
<Label for="login-email">{t('auth.email')}</Label>
|
|
62
|
+
<div class="relative">
|
|
63
|
+
<Mail class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
61
64
|
<Input
|
|
62
65
|
id="login-email"
|
|
63
66
|
type="email"
|
|
@@ -69,58 +72,37 @@
|
|
|
69
72
|
</div>
|
|
70
73
|
</div>
|
|
71
74
|
|
|
72
|
-
<div
|
|
73
|
-
<div class="flex items-center justify-between">
|
|
74
|
-
<
|
|
75
|
+
<div>
|
|
76
|
+
<div class="flex items-center justify-between mb-2">
|
|
77
|
+
<Label for="login-password">{t('auth.password')}</Label>
|
|
75
78
|
{#if authProvider.forgotPassword}
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
onclick={() => navigate('/forgot-password')}
|
|
80
|
-
>{t('auth.forgotPasswordLink')}</button>
|
|
79
|
+
<Button variant="link" class="text-xs h-auto p-0" onclick={() => navigate('/forgot-password')}>
|
|
80
|
+
{t('auth.forgotPasswordLink')}
|
|
81
|
+
</Button>
|
|
81
82
|
{/if}
|
|
82
83
|
</div>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
bind:value={password}
|
|
90
|
-
class="pl-9 pr-9"
|
|
91
|
-
autocomplete="current-password"
|
|
92
|
-
/>
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
95
|
-
class="password-toggle"
|
|
96
|
-
onclick={() => showPassword = !showPassword}
|
|
97
|
-
tabindex={-1}
|
|
98
|
-
>
|
|
99
|
-
{#if showPassword}
|
|
100
|
-
<EyeOff class="h-4 w-4" />
|
|
101
|
-
{:else}
|
|
102
|
-
<Eye class="h-4 w-4" />
|
|
103
|
-
{/if}
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
84
|
+
<PasswordInput
|
|
85
|
+
id="login-password"
|
|
86
|
+
label=""
|
|
87
|
+
bind:value={password}
|
|
88
|
+
autocomplete="current-password"
|
|
89
|
+
/>
|
|
106
90
|
</div>
|
|
107
91
|
|
|
108
92
|
<Button type="submit" class="w-full" disabled={login.isLoading}>
|
|
109
93
|
{#if login.isLoading}
|
|
110
|
-
<
|
|
94
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
111
95
|
{/if}
|
|
112
96
|
{t('auth.loginButton')}
|
|
113
97
|
</Button>
|
|
114
98
|
</form>
|
|
115
99
|
|
|
116
100
|
{#if authProvider.register}
|
|
117
|
-
<div class="
|
|
101
|
+
<div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
|
|
118
102
|
<span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
onclick={() => navigate('/register')}
|
|
123
|
-
>{t('auth.register')}</button>
|
|
103
|
+
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/register')}>
|
|
104
|
+
{t('auth.register')}
|
|
105
|
+
</Button>
|
|
124
106
|
</div>
|
|
125
107
|
{/if}
|
|
126
108
|
</Card.CardContent>
|
|
@@ -131,106 +113,3 @@
|
|
|
131
113
|
</p>
|
|
132
114
|
</div>
|
|
133
115
|
</div>
|
|
134
|
-
|
|
135
|
-
<style>
|
|
136
|
-
.login-page {
|
|
137
|
-
min-height: 100vh;
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
justify-content: center;
|
|
141
|
-
background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
|
|
142
|
-
padding: 1rem;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.login-container {
|
|
146
|
-
width: 100%;
|
|
147
|
-
max-width: 420px;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
:global(.login-card) {
|
|
151
|
-
backdrop-filter: blur(20px);
|
|
152
|
-
border: 1px solid hsl(var(--border) / 0.5);
|
|
153
|
-
box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
:global(.login-header) {
|
|
157
|
-
text-align: center;
|
|
158
|
-
padding-bottom: 0.5rem;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.login-icon {
|
|
162
|
-
display: inline-flex;
|
|
163
|
-
align-items: center;
|
|
164
|
-
justify-content: center;
|
|
165
|
-
width: 48px;
|
|
166
|
-
height: 48px;
|
|
167
|
-
border-radius: 12px;
|
|
168
|
-
background: hsl(var(--primary) / 0.1);
|
|
169
|
-
color: hsl(var(--primary));
|
|
170
|
-
margin: 0 auto 0.75rem;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.input-with-icon {
|
|
174
|
-
position: relative;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
:global(.input-icon) {
|
|
178
|
-
position: absolute;
|
|
179
|
-
left: 0.75rem;
|
|
180
|
-
top: 50%;
|
|
181
|
-
transform: translateY(-50%);
|
|
182
|
-
color: hsl(var(--muted-foreground));
|
|
183
|
-
pointer-events: none;
|
|
184
|
-
z-index: 1;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.password-toggle {
|
|
188
|
-
position: absolute;
|
|
189
|
-
right: 0.75rem;
|
|
190
|
-
top: 50%;
|
|
191
|
-
transform: translateY(-50%);
|
|
192
|
-
color: hsl(var(--muted-foreground));
|
|
193
|
-
background: none;
|
|
194
|
-
border: none;
|
|
195
|
-
cursor: pointer;
|
|
196
|
-
padding: 2px;
|
|
197
|
-
z-index: 1;
|
|
198
|
-
}
|
|
199
|
-
.password-toggle:hover {
|
|
200
|
-
color: hsl(var(--foreground));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.error-alert {
|
|
204
|
-
padding: 0.75rem;
|
|
205
|
-
border-radius: 0.5rem;
|
|
206
|
-
background: hsl(var(--destructive) / 0.1);
|
|
207
|
-
border: 1px solid hsl(var(--destructive) / 0.3);
|
|
208
|
-
color: hsl(var(--destructive));
|
|
209
|
-
font-size: 0.875rem;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.loading-spinner {
|
|
213
|
-
display: inline-block;
|
|
214
|
-
width: 16px;
|
|
215
|
-
height: 16px;
|
|
216
|
-
border: 2px solid transparent;
|
|
217
|
-
border-top-color: currentColor;
|
|
218
|
-
border-radius: 50%;
|
|
219
|
-
animation: spin 0.6s linear infinite;
|
|
220
|
-
margin-right: 0.5rem;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
@keyframes spin {
|
|
224
|
-
to { transform: rotate(360deg); }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.auth-link {
|
|
228
|
-
display: flex;
|
|
229
|
-
align-items: center;
|
|
230
|
-
justify-content: center;
|
|
231
|
-
gap: 0.25rem;
|
|
232
|
-
margin-top: 1.25rem;
|
|
233
|
-
padding-top: 1.25rem;
|
|
234
|
-
border-top: 1px solid hsl(var(--border));
|
|
235
|
-
}
|
|
236
|
-
</style>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { FieldDefinition } from '@svadmin/core';
|
|
4
4
|
import { getResource, useForm } from '@svadmin/core';
|
|
5
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
6
|
import * as Dialog from './ui/dialog/index.js';
|
|
6
7
|
import AutoForm from './AutoForm.svelte';
|
|
7
8
|
|
|
@@ -14,18 +15,21 @@
|
|
|
14
15
|
}>();
|
|
15
16
|
|
|
16
17
|
const resource = $derived(getResource(resourceName));
|
|
18
|
+
const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
|
|
19
|
+
const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
|
|
17
20
|
|
|
18
21
|
function handleClose() {
|
|
19
22
|
open = false;
|
|
23
|
+
onSuccess?.();
|
|
20
24
|
}
|
|
21
25
|
</script>
|
|
22
26
|
|
|
23
27
|
{#if open}
|
|
24
28
|
<Dialog.Dialog bind:open>
|
|
25
|
-
<Dialog.DialogContent class="
|
|
29
|
+
<Dialog.DialogContent class="{dialogWidth} max-h-[85vh] overflow-y-auto">
|
|
26
30
|
<Dialog.DialogHeader>
|
|
27
31
|
<Dialog.DialogTitle>
|
|
28
|
-
{mode === 'create' ?
|
|
32
|
+
{mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
|
|
29
33
|
</Dialog.DialogTitle>
|
|
30
34
|
</Dialog.DialogHeader>
|
|
31
35
|
<AutoForm {resourceName} {mode} {id} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
2
3
|
import { Skeleton } from './ui/skeleton';
|
|
3
4
|
|
|
4
5
|
let {
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
}>();
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
|
-
<div class="w-full space-y-6">
|
|
14
|
+
<div class="w-full space-y-6" out:fade={{ duration: 200 }}>
|
|
14
15
|
<!-- Header Skeleton -->
|
|
15
16
|
<div class="flex items-center justify-between">
|
|
16
17
|
<div class="space-y-2">
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Input } from './ui/input/index.js';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
5
|
+
import { Label } from './ui/label/index.js';
|
|
6
|
+
import { Progress } from './ui/progress/index.js';
|
|
7
|
+
import { Lock, Eye, EyeOff } from 'lucide-svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
autocomplete?: string;
|
|
15
|
+
showStrength?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
id,
|
|
21
|
+
label,
|
|
22
|
+
value = $bindable(''),
|
|
23
|
+
placeholder = '••••••••',
|
|
24
|
+
autocomplete = 'current-password',
|
|
25
|
+
showStrength = false,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let showPassword = $state(false);
|
|
30
|
+
|
|
31
|
+
// Password strength calculation
|
|
32
|
+
const strength = $derived.by(() => {
|
|
33
|
+
if (!showStrength || !value) return 0;
|
|
34
|
+
let score = 0;
|
|
35
|
+
if (value.length >= 8) score += 25;
|
|
36
|
+
if (value.length >= 12) score += 10;
|
|
37
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 25;
|
|
38
|
+
if (/\d/.test(value)) score += 20;
|
|
39
|
+
if (/[^a-zA-Z0-9]/.test(value)) score += 20;
|
|
40
|
+
return Math.min(score, 100);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const strengthColor = $derived(
|
|
44
|
+
strength < 30 ? 'bg-destructive' :
|
|
45
|
+
strength < 60 ? 'bg-amber-500' :
|
|
46
|
+
strength < 80 ? 'bg-blue-500' : 'bg-emerald-500'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const strengthLabel = $derived(
|
|
50
|
+
strength < 30 ? 'Weak' :
|
|
51
|
+
strength < 60 ? 'Fair' :
|
|
52
|
+
strength < 80 ? 'Good' : 'Strong'
|
|
53
|
+
);
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="space-y-2 {className}">
|
|
57
|
+
<Label for={id}>{label}</Label>
|
|
58
|
+
<div class="relative">
|
|
59
|
+
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
60
|
+
<Input
|
|
61
|
+
{id}
|
|
62
|
+
type={showPassword ? 'text' : 'password'}
|
|
63
|
+
{placeholder}
|
|
64
|
+
bind:value
|
|
65
|
+
class="pl-9 pr-9"
|
|
66
|
+
{autocomplete}
|
|
67
|
+
/>
|
|
68
|
+
<TooltipButton
|
|
69
|
+
tooltip={showPassword ? 'Hide password' : 'Show password'}
|
|
70
|
+
variant="ghost"
|
|
71
|
+
size="icon"
|
|
72
|
+
type="button"
|
|
73
|
+
class="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 z-[1]"
|
|
74
|
+
onclick={() => showPassword = !showPassword}
|
|
75
|
+
tabindex={-1}
|
|
76
|
+
>
|
|
77
|
+
{#if showPassword}
|
|
78
|
+
<EyeOff class="h-4 w-4" />
|
|
79
|
+
{:else}
|
|
80
|
+
<Eye class="h-4 w-4" />
|
|
81
|
+
{/if}
|
|
82
|
+
</TooltipButton>
|
|
83
|
+
</div>
|
|
84
|
+
{#if showStrength && value}
|
|
85
|
+
<div class="flex items-center gap-2">
|
|
86
|
+
<div class="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
|
87
|
+
<div
|
|
88
|
+
class="h-full rounded-full transition-all duration-300 {strengthColor}"
|
|
89
|
+
style="width: {strength}%"
|
|
90
|
+
></div>
|
|
91
|
+
</div>
|
|
92
|
+
<span class="text-xs text-muted-foreground w-12 text-right">{strengthLabel}</span>
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|