@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.
- package/package.json +1 -1
- package/src/app.css +4 -48
- package/src/components/AdminApp.svelte +37 -32
- package/src/components/AutoForm.svelte +62 -10
- package/src/components/AutoTable.svelte +12 -12
- package/src/components/CanAccess.svelte +21 -0
- package/src/components/DevTools.svelte +231 -0
- package/src/components/DrawerForm.svelte +26 -0
- package/src/components/ForgotPasswordPage.svelte +213 -0
- package/src/components/Layout.svelte +2 -2
- package/src/components/LoginPage.svelte +245 -0
- package/src/components/ModalForm.svelte +34 -0
- package/src/components/RegisterPage.svelte +252 -0
- package/src/components/ShowPage.svelte +3 -3
- package/src/components/Sidebar.svelte +43 -7
- package/src/components/UndoableNotification.svelte +132 -0
- package/src/index.ts +8 -0
- package/src/router-state.svelte.ts +39 -0
|
@@ -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-
|
|
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}
|