@svadmin/ui 0.0.1 → 0.0.3

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.
@@ -0,0 +1,213 @@
1
+ <script lang="ts">
2
+ import type { AuthProvider } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import { navigate } from '@svadmin/core/router';
5
+ import { toast } from '@svadmin/core/toast';
6
+ import { Button } from './ui/button/index.js';
7
+ import { Input } from './ui/input/index.js';
8
+ import * as Card from './ui/card/index.js';
9
+ import { KeyRound, Mail, ArrowLeft, CheckCircle } from 'lucide-svelte';
10
+
11
+ let { authProvider, title = 'Admin' } = $props<{
12
+ authProvider: AuthProvider;
13
+ title?: string;
14
+ }>();
15
+
16
+ let email = $state('');
17
+ let loading = $state(false);
18
+ let error = $state('');
19
+ let sent = $state(false);
20
+
21
+ async function handleSubmit(e: SubmitEvent) {
22
+ e.preventDefault();
23
+ error = '';
24
+
25
+ if (!email) { error = t('auth.emailRequired'); return; }
26
+
27
+ loading = true;
28
+ try {
29
+ const result = await authProvider.forgotPassword!({ email });
30
+ if (result.success) {
31
+ sent = true;
32
+ toast.success(t('auth.resetLinkSent'));
33
+ } else {
34
+ error = result.error?.message ?? t('common.operationFailed');
35
+ }
36
+ } catch (err) {
37
+ error = err instanceof Error ? err.message : t('common.operationFailed');
38
+ toast.error(error);
39
+ } finally {
40
+ loading = false;
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <div class="forgot-page">
46
+ <div class="forgot-container">
47
+ <Card.Card class="login-card">
48
+ <Card.CardHeader class="login-header">
49
+ <div class="forgot-icon">
50
+ {#if sent}
51
+ <CheckCircle class="h-6 w-6" />
52
+ {:else}
53
+ <KeyRound class="h-6 w-6" />
54
+ {/if}
55
+ </div>
56
+ <Card.CardTitle class="text-2xl font-bold">
57
+ {sent ? t('auth.resetLinkSent') : t('auth.forgotPassword')}
58
+ </Card.CardTitle>
59
+ {#if !sent}
60
+ <p class="text-sm text-muted-foreground">{t('auth.forgotPasswordDescription')}</p>
61
+ {/if}
62
+ </Card.CardHeader>
63
+ <Card.CardContent>
64
+ {#if sent}
65
+ <div class="success-state">
66
+ <p class="text-sm text-muted-foreground text-center mb-4">
67
+ {t('auth.resetLinkSent')}
68
+ </p>
69
+ <Button variant="outline" class="w-full" onclick={() => navigate('/login')}>
70
+ <ArrowLeft class="h-4 w-4 mr-2" />
71
+ {t('auth.backToLogin')}
72
+ </Button>
73
+ </div>
74
+ {:else}
75
+ <form onsubmit={handleSubmit} class="space-y-4">
76
+ {#if error}
77
+ <div class="error-alert">
78
+ <p>{error}</p>
79
+ </div>
80
+ {/if}
81
+
82
+ <div class="space-y-2">
83
+ <label for="forgot-email" class="text-sm font-medium text-foreground">{t('auth.email')}</label>
84
+ <div class="input-with-icon">
85
+ <Mail class="input-icon h-4 w-4" />
86
+ <Input
87
+ id="forgot-email"
88
+ type="email"
89
+ placeholder="name@example.com"
90
+ bind:value={email}
91
+ class="pl-9"
92
+ autocomplete="email"
93
+ />
94
+ </div>
95
+ </div>
96
+
97
+ <Button type="submit" class="w-full" disabled={loading}>
98
+ {#if loading}
99
+ <span class="loading-spinner"></span>
100
+ {/if}
101
+ {t('auth.sendResetLink')}
102
+ </Button>
103
+ </form>
104
+
105
+ <div class="auth-link">
106
+ <button
107
+ type="button"
108
+ class="text-sm text-primary hover:underline font-medium inline-flex items-center gap-1"
109
+ onclick={() => navigate('/login')}
110
+ >
111
+ <ArrowLeft class="h-3 w-3" />
112
+ {t('auth.backToLogin')}
113
+ </button>
114
+ </div>
115
+ {/if}
116
+ </Card.CardContent>
117
+ </Card.Card>
118
+
119
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
120
+ Powered by {title}
121
+ </p>
122
+ </div>
123
+ </div>
124
+
125
+ <style>
126
+ .forgot-page {
127
+ min-height: 100vh;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
132
+ padding: 1rem;
133
+ }
134
+
135
+ .forgot-container {
136
+ width: 100%;
137
+ max-width: 420px;
138
+ }
139
+
140
+ .forgot-icon {
141
+ display: inline-flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ width: 48px;
145
+ height: 48px;
146
+ border-radius: 12px;
147
+ background: hsl(var(--primary) / 0.1);
148
+ color: hsl(var(--primary));
149
+ margin: 0 auto 0.75rem;
150
+ }
151
+
152
+ :global(.login-card) {
153
+ backdrop-filter: blur(20px);
154
+ border: 1px solid hsl(var(--border) / 0.5);
155
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
156
+ }
157
+
158
+ :global(.login-header) {
159
+ text-align: center;
160
+ padding-bottom: 0.5rem;
161
+ }
162
+
163
+ .input-with-icon {
164
+ position: relative;
165
+ }
166
+
167
+ .input-icon {
168
+ position: absolute;
169
+ left: 0.75rem;
170
+ top: 50%;
171
+ transform: translateY(-50%);
172
+ color: hsl(var(--muted-foreground));
173
+ pointer-events: none;
174
+ z-index: 1;
175
+ }
176
+
177
+ .error-alert {
178
+ padding: 0.75rem;
179
+ border-radius: 0.5rem;
180
+ background: hsl(var(--destructive) / 0.1);
181
+ border: 1px solid hsl(var(--destructive) / 0.3);
182
+ color: hsl(var(--destructive));
183
+ font-size: 0.875rem;
184
+ }
185
+
186
+ .success-state {
187
+ padding: 0.5rem 0;
188
+ }
189
+
190
+ .loading-spinner {
191
+ display: inline-block;
192
+ width: 16px;
193
+ height: 16px;
194
+ border: 2px solid transparent;
195
+ border-top-color: currentColor;
196
+ border-radius: 50%;
197
+ animation: spin 0.6s linear infinite;
198
+ margin-right: 0.5rem;
199
+ }
200
+
201
+ @keyframes spin {
202
+ to { transform: rotate(360deg); }
203
+ }
204
+
205
+ .auth-link {
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ margin-top: 1.25rem;
210
+ padding-top: 1.25rem;
211
+ border-top: 1px solid hsl(var(--border));
212
+ }
213
+ </style>
@@ -51,10 +51,10 @@
51
51
  <Loader2 class="h-8 w-8 animate-spin text-primary" />
52
52
  </div>
53
53
  {:else}
54
- <div class="flex h-screen">
54
+ <div class="flex h-screen bg-gradient-to-br from-background via-background to-muted/30">
55
55
  <Sidebar {collapsed} {identity} {title} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
56
56
  <main
57
- class="flex-1 overflow-y-auto p-6 transition-all duration-300"
57
+ class="flex-1 overflow-y-auto p-8 transition-all duration-300"
58
58
  class:ml-64={!collapsed}
59
59
  class:ml-16={collapsed}
60
60
  >
@@ -0,0 +1,245 @@
1
+ <script lang="ts">
2
+ import type { AuthProvider } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import { navigate } from '@svadmin/core/router';
5
+ import { toast } from '@svadmin/core/toast';
6
+ import { Button } from './ui/button/index.js';
7
+ import { Input } from './ui/input/index.js';
8
+ import * as Card from './ui/card/index.js';
9
+ import { LogIn, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
10
+
11
+ let { authProvider, title = 'Admin', onSuccess } = $props<{
12
+ authProvider: AuthProvider;
13
+ title?: string;
14
+ onSuccess?: () => void;
15
+ }>();
16
+
17
+ let email = $state('');
18
+ let password = $state('');
19
+ let loading = $state(false);
20
+ let showPassword = $state(false);
21
+ let error = $state('');
22
+
23
+ async function handleSubmit(e: SubmitEvent) {
24
+ e.preventDefault();
25
+ error = '';
26
+
27
+ if (!email) { error = t('auth.emailRequired'); return; }
28
+ if (!password) { error = t('auth.passwordRequired'); return; }
29
+
30
+ loading = true;
31
+ try {
32
+ const result = await authProvider.login({ email, password });
33
+ if (result.success) {
34
+ onSuccess?.();
35
+ if (result.redirectTo) navigate(result.redirectTo);
36
+ } else {
37
+ error = result.error?.message ?? t('common.loginFailed');
38
+ }
39
+ } catch (err) {
40
+ error = err instanceof Error ? err.message : t('common.loginFailed');
41
+ toast.error(error);
42
+ } finally {
43
+ loading = false;
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <div class="login-page">
49
+ <div class="login-container">
50
+ <Card.Card class="login-card">
51
+ <Card.CardHeader class="login-header">
52
+ <div class="login-icon">
53
+ <LogIn class="h-6 w-6" />
54
+ </div>
55
+ <Card.CardTitle class="text-2xl font-bold">{t('auth.welcomeBack')}</Card.CardTitle>
56
+ <p class="text-sm text-muted-foreground">{t('auth.welcomeMessage')}</p>
57
+ </Card.CardHeader>
58
+ <Card.CardContent>
59
+ <form onsubmit={handleSubmit} class="space-y-4">
60
+ {#if error}
61
+ <div class="error-alert">
62
+ <p>{error}</p>
63
+ </div>
64
+ {/if}
65
+
66
+ <div class="space-y-2">
67
+ <label for="login-email" class="text-sm font-medium text-foreground">{t('auth.email')}</label>
68
+ <div class="input-with-icon">
69
+ <Mail class="input-icon h-4 w-4" />
70
+ <Input
71
+ id="login-email"
72
+ type="email"
73
+ placeholder="name@example.com"
74
+ bind:value={email}
75
+ class="pl-9"
76
+ autocomplete="email"
77
+ />
78
+ </div>
79
+ </div>
80
+
81
+ <div class="space-y-2">
82
+ <div class="flex items-center justify-between">
83
+ <label for="login-password" class="text-sm font-medium text-foreground">{t('auth.password')}</label>
84
+ {#if authProvider.forgotPassword}
85
+ <button
86
+ type="button"
87
+ class="text-xs text-primary hover:underline"
88
+ onclick={() => navigate('/forgot-password')}
89
+ >{t('auth.forgotPasswordLink')}</button>
90
+ {/if}
91
+ </div>
92
+ <div class="input-with-icon">
93
+ <Lock class="input-icon h-4 w-4" />
94
+ <Input
95
+ id="login-password"
96
+ type={showPassword ? 'text' : 'password'}
97
+ placeholder="••••••••"
98
+ bind:value={password}
99
+ class="pl-9 pr-9"
100
+ autocomplete="current-password"
101
+ />
102
+ <button
103
+ type="button"
104
+ class="password-toggle"
105
+ onclick={() => showPassword = !showPassword}
106
+ tabindex={-1}
107
+ >
108
+ {#if showPassword}
109
+ <EyeOff class="h-4 w-4" />
110
+ {:else}
111
+ <Eye class="h-4 w-4" />
112
+ {/if}
113
+ </button>
114
+ </div>
115
+ </div>
116
+
117
+ <Button type="submit" class="w-full" disabled={loading}>
118
+ {#if loading}
119
+ <span class="loading-spinner"></span>
120
+ {/if}
121
+ {t('auth.loginButton')}
122
+ </Button>
123
+ </form>
124
+
125
+ {#if authProvider.register}
126
+ <div class="auth-link">
127
+ <span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
128
+ <button
129
+ type="button"
130
+ class="text-sm text-primary hover:underline font-medium"
131
+ onclick={() => navigate('/register')}
132
+ >{t('auth.register')}</button>
133
+ </div>
134
+ {/if}
135
+ </Card.CardContent>
136
+ </Card.Card>
137
+
138
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
139
+ Powered by {title}
140
+ </p>
141
+ </div>
142
+ </div>
143
+
144
+ <style>
145
+ .login-page {
146
+ min-height: 100vh;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
151
+ padding: 1rem;
152
+ }
153
+
154
+ .login-container {
155
+ width: 100%;
156
+ max-width: 420px;
157
+ }
158
+
159
+ :global(.login-card) {
160
+ backdrop-filter: blur(20px);
161
+ border: 1px solid hsl(var(--border) / 0.5);
162
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
163
+ }
164
+
165
+ :global(.login-header) {
166
+ text-align: center;
167
+ padding-bottom: 0.5rem;
168
+ }
169
+
170
+ .login-icon {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ width: 48px;
175
+ height: 48px;
176
+ border-radius: 12px;
177
+ background: hsl(var(--primary) / 0.1);
178
+ color: hsl(var(--primary));
179
+ margin: 0 auto 0.75rem;
180
+ }
181
+
182
+ .input-with-icon {
183
+ position: relative;
184
+ }
185
+
186
+ .input-icon {
187
+ position: absolute;
188
+ left: 0.75rem;
189
+ top: 50%;
190
+ transform: translateY(-50%);
191
+ color: hsl(var(--muted-foreground));
192
+ pointer-events: none;
193
+ z-index: 1;
194
+ }
195
+
196
+ .password-toggle {
197
+ position: absolute;
198
+ right: 0.75rem;
199
+ top: 50%;
200
+ transform: translateY(-50%);
201
+ color: hsl(var(--muted-foreground));
202
+ background: none;
203
+ border: none;
204
+ cursor: pointer;
205
+ padding: 2px;
206
+ z-index: 1;
207
+ }
208
+ .password-toggle:hover {
209
+ color: hsl(var(--foreground));
210
+ }
211
+
212
+ .error-alert {
213
+ padding: 0.75rem;
214
+ border-radius: 0.5rem;
215
+ background: hsl(var(--destructive) / 0.1);
216
+ border: 1px solid hsl(var(--destructive) / 0.3);
217
+ color: hsl(var(--destructive));
218
+ font-size: 0.875rem;
219
+ }
220
+
221
+ .loading-spinner {
222
+ display: inline-block;
223
+ width: 16px;
224
+ height: 16px;
225
+ border: 2px solid transparent;
226
+ border-top-color: currentColor;
227
+ border-radius: 50%;
228
+ animation: spin 0.6s linear infinite;
229
+ margin-right: 0.5rem;
230
+ }
231
+
232
+ @keyframes spin {
233
+ to { transform: rotate(360deg); }
234
+ }
235
+
236
+ .auth-link {
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ gap: 0.25rem;
241
+ margin-top: 1.25rem;
242
+ padding-top: 1.25rem;
243
+ border-top: 1px solid hsl(var(--border));
244
+ }
245
+ </style>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { FieldDefinition } from '@svadmin/core';
4
+ import { getResource, useForm } from '@svadmin/core';
5
+ import * as Dialog from './ui/dialog/index.js';
6
+ import AutoForm from './AutoForm.svelte';
7
+
8
+ let { resourceName, mode = 'create', id, open = $bindable(false), onSuccess } = $props<{
9
+ resourceName: string;
10
+ mode?: 'create' | 'edit';
11
+ id?: string | number;
12
+ open: boolean;
13
+ onSuccess?: () => void;
14
+ }>();
15
+
16
+ const resource = $derived(getResource(resourceName));
17
+
18
+ function handleClose() {
19
+ open = false;
20
+ }
21
+ </script>
22
+
23
+ {#if open}
24
+ <Dialog.Dialog bind:open>
25
+ <Dialog.DialogContent class="sm:max-w-2xl max-h-[85vh] overflow-y-auto">
26
+ <Dialog.DialogHeader>
27
+ <Dialog.DialogTitle>
28
+ {mode === 'create' ? `Create ${resource.label}` : `Edit ${resource.label}`}
29
+ </Dialog.DialogTitle>
30
+ </Dialog.DialogHeader>
31
+ <AutoForm {resourceName} {mode} {id} />
32
+ </Dialog.DialogContent>
33
+ </Dialog.Dialog>
34
+ {/if}