@svadmin/ui 0.0.3 → 0.3.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 +3 -2
- package/src/components/AdminApp.svelte +31 -21
- package/src/components/Authenticated.svelte +57 -0
- package/src/components/AutoForm.svelte +5 -5
- package/src/components/AutoSaveIndicator.svelte +30 -0
- package/src/components/AutoTable.svelte +81 -10
- package/src/components/Breadcrumbs.svelte +2 -0
- package/src/components/CatchAllNavigate.svelte +21 -0
- package/src/components/ConfigErrorScreen.svelte +224 -0
- package/src/components/ConfirmDialog.svelte +1 -1
- package/src/components/CreatePage.svelte +42 -0
- package/src/components/DevTools.svelte +83 -34
- package/src/components/EditPage.svelte +52 -0
- package/src/components/EmptyState.svelte +17 -31
- package/src/components/ErrorComponent.svelte +35 -0
- package/src/components/ForgotPasswordPage.svelte +12 -22
- package/src/components/Header.svelte +28 -59
- package/src/components/InferencerPanel.svelte +166 -0
- package/src/components/ListPage.svelte +47 -0
- package/src/components/LiveIndicator.svelte +115 -0
- package/src/components/LoginPage.svelte +13 -22
- package/src/components/NavigateToResource.svelte +20 -0
- package/src/components/PageSkeleton.svelte +70 -0
- package/src/components/RegisterPage.svelte +12 -23
- package/src/components/ShowPage.svelte +3 -3
- package/src/components/Sidebar.svelte +29 -10
- package/src/components/StatsCard.svelte +23 -2
- package/src/components/ThemedTitle.svelte +32 -0
- package/src/components/Toast.svelte +21 -11
- package/src/components/UpdatePasswordPage.svelte +217 -0
- package/src/components/buttons/CloneButton.svelte +30 -0
- package/src/components/buttons/CreateButton.svelte +29 -0
- package/src/components/buttons/DeleteButton.svelte +59 -0
- package/src/components/buttons/EditButton.svelte +30 -0
- package/src/components/buttons/ExportButton.svelte +24 -0
- package/src/components/buttons/ImportButton.svelte +50 -0
- package/src/components/buttons/ListButton.svelte +23 -0
- package/src/components/buttons/RefreshButton.svelte +31 -0
- package/src/components/buttons/SaveButton.svelte +27 -0
- package/src/components/buttons/ShowButton.svelte +24 -0
- package/src/components/buttons/index.ts +10 -0
- package/src/components/fields/BooleanField.svelte +13 -0
- package/src/components/fields/DateField.svelte +6 -0
- package/src/components/fields/EmailField.svelte +15 -0
- package/src/components/fields/FileField.svelte +19 -0
- package/src/components/fields/ImageField.svelte +14 -0
- package/src/components/fields/MarkdownField.svelte +19 -0
- package/src/components/fields/NumberField.svelte +6 -0
- package/src/components/fields/TagField.svelte +17 -0
- package/src/components/fields/TextField.svelte +16 -0
- package/src/components/fields/UrlField.svelte +20 -0
- package/src/components/ui/avatar/avatar.svelte +2 -1
- package/src/index.ts +32 -0
- package/src/router-state.svelte.ts +33 -7
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import { AlertTriangle, Copy, CheckCircle } from 'lucide-svelte';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import * as Card from './ui/card/index.js';
|
|
6
|
+
|
|
7
|
+
let { title = 'Configuration Required', missingVars = [], envTemplate = '' } = $props<{
|
|
8
|
+
title?: string;
|
|
9
|
+
missingVars?: { key: string; description?: string }[];
|
|
10
|
+
envTemplate?: string;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
let copied = $state<Record<string, boolean>>({});
|
|
14
|
+
|
|
15
|
+
async function copyToClipboard(text: string, key: string) {
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(text);
|
|
18
|
+
copied = { ...copied, [key]: true };
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
copied = { ...copied, [key]: false };
|
|
21
|
+
}, 2000);
|
|
22
|
+
} catch {
|
|
23
|
+
console.warn('[svadmin] clipboard API unavailable');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function copyAll() {
|
|
28
|
+
const text = envTemplate || missingVars.map((v: { key: string }) => `${v.key}=`).join('\n');
|
|
29
|
+
await copyToClipboard(text, '__all__');
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="config-error-page">
|
|
34
|
+
<div class="config-error-container">
|
|
35
|
+
<Card.Card class="config-error-card">
|
|
36
|
+
<Card.CardHeader class="config-error-header">
|
|
37
|
+
<div class="config-error-icon">
|
|
38
|
+
<AlertTriangle class="h-7 w-7" />
|
|
39
|
+
</div>
|
|
40
|
+
<Card.CardTitle class="text-xl font-bold">{title}</Card.CardTitle>
|
|
41
|
+
<p class="text-sm text-muted-foreground">
|
|
42
|
+
{t('config.missingEnvDescription')}
|
|
43
|
+
</p>
|
|
44
|
+
</Card.CardHeader>
|
|
45
|
+
<Card.CardContent class="space-y-4">
|
|
46
|
+
{#if missingVars.length > 0}
|
|
47
|
+
<div class="env-var-list">
|
|
48
|
+
{#each missingVars as v (v.key)}
|
|
49
|
+
<div class="env-var-row">
|
|
50
|
+
<div class="env-var-info">
|
|
51
|
+
<code class="env-var-key">{v.key}</code>
|
|
52
|
+
{#if v.description}
|
|
53
|
+
<span class="env-var-desc">{v.description}</span>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
<button
|
|
57
|
+
class="copy-btn"
|
|
58
|
+
onclick={() => copyToClipboard(`${v.key}=`, v.key)}
|
|
59
|
+
title="Copy"
|
|
60
|
+
>
|
|
61
|
+
{#if copied[v.key]}
|
|
62
|
+
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
63
|
+
{:else}
|
|
64
|
+
<Copy class="h-3.5 w-3.5" />
|
|
65
|
+
{/if}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
{/each}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
{#if envTemplate}
|
|
73
|
+
<div class="env-template">
|
|
74
|
+
<div class="env-template-header">
|
|
75
|
+
<span class="text-xs font-medium text-muted-foreground">{t('config.envFilePath')}</span>
|
|
76
|
+
<button
|
|
77
|
+
class="copy-btn"
|
|
78
|
+
onclick={copyAll}
|
|
79
|
+
>
|
|
80
|
+
{#if copied['__all__']}
|
|
81
|
+
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
82
|
+
<span class="text-xs">Copied!</span>
|
|
83
|
+
{:else}
|
|
84
|
+
<Copy class="h-3.5 w-3.5" />
|
|
85
|
+
<span class="text-xs">Copy All</span>
|
|
86
|
+
{/if}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
<pre class="env-template-code">{envTemplate}</pre>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<p class="text-xs text-muted-foreground text-center mt-4">
|
|
94
|
+
{t('config.reload')}
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
<Button variant="outline" class="w-full" onclick={() => window.location.reload()}>
|
|
98
|
+
Reload Page
|
|
99
|
+
</Button>
|
|
100
|
+
</Card.CardContent>
|
|
101
|
+
</Card.Card>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.config-error-page {
|
|
107
|
+
min-height: 100vh;
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
background: linear-gradient(135deg, hsl(var(--destructive) / 0.05) 0%, hsl(var(--background)) 50%, hsl(var(--destructive) / 0.03) 100%);
|
|
112
|
+
padding: 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.config-error-container {
|
|
116
|
+
width: 100%;
|
|
117
|
+
max-width: 480px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:global(.config-error-card) {
|
|
121
|
+
backdrop-filter: blur(20px);
|
|
122
|
+
border: 1px solid hsl(var(--border) / 0.5);
|
|
123
|
+
box-shadow: 0 8px 32px hsl(var(--destructive) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:global(.config-error-header) {
|
|
127
|
+
text-align: center;
|
|
128
|
+
padding-bottom: 0.5rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.config-error-icon {
|
|
132
|
+
display: inline-flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
width: 56px;
|
|
136
|
+
height: 56px;
|
|
137
|
+
border-radius: 14px;
|
|
138
|
+
background: hsl(var(--destructive) / 0.1);
|
|
139
|
+
color: hsl(var(--destructive));
|
|
140
|
+
margin: 0 auto 0.75rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.env-var-list {
|
|
144
|
+
border: 1px solid hsl(var(--border));
|
|
145
|
+
border-radius: 0.5rem;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.env-var-row {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
padding: 0.625rem 0.75rem;
|
|
154
|
+
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
|
155
|
+
gap: 0.5rem;
|
|
156
|
+
}
|
|
157
|
+
.env-var-row:last-child {
|
|
158
|
+
border-bottom: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.env-var-info {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 0.125rem;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.env-var-key {
|
|
169
|
+
font-size: 0.8125rem;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
color: hsl(var(--foreground));
|
|
172
|
+
font-family: monospace;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.env-var-desc {
|
|
176
|
+
font-size: 0.6875rem;
|
|
177
|
+
color: hsl(var(--muted-foreground));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.copy-btn {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 0.25rem;
|
|
184
|
+
background: none;
|
|
185
|
+
border: none;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
padding: 0.25rem 0.5rem;
|
|
188
|
+
color: hsl(var(--muted-foreground));
|
|
189
|
+
border-radius: 0.25rem;
|
|
190
|
+
transition: all 0.15s;
|
|
191
|
+
flex-shrink: 0;
|
|
192
|
+
}
|
|
193
|
+
.copy-btn:hover {
|
|
194
|
+
color: hsl(var(--foreground));
|
|
195
|
+
background: hsl(var(--muted) / 0.5);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.env-template {
|
|
199
|
+
border: 1px solid hsl(var(--border));
|
|
200
|
+
border-radius: 0.5rem;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.env-template-header {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: space-between;
|
|
208
|
+
padding: 0.5rem 0.75rem;
|
|
209
|
+
background: hsl(var(--muted) / 0.5);
|
|
210
|
+
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.env-template-code {
|
|
214
|
+
padding: 0.75rem;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
font-family: monospace;
|
|
217
|
+
line-height: 1.6;
|
|
218
|
+
color: hsl(var(--foreground));
|
|
219
|
+
background: hsl(var(--muted) / 0.2);
|
|
220
|
+
margin: 0;
|
|
221
|
+
white-space: pre-wrap;
|
|
222
|
+
word-break: break-all;
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<Button variant="outline" onclick={oncancel}>
|
|
38
38
|
{resolvedCancelText}
|
|
39
39
|
</Button>
|
|
40
|
-
<Button variant={variantMap[variant]} onclick={onconfirm}>
|
|
40
|
+
<Button variant={variantMap[variant as keyof typeof variantMap]} onclick={onconfirm}>
|
|
41
41
|
{resolvedConfirmText}
|
|
42
42
|
</Button>
|
|
43
43
|
</Dialog.Footer>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getResource } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import PageHeader from './PageHeader.svelte';
|
|
7
|
+
import AutoForm from './AutoForm.svelte';
|
|
8
|
+
import { ArrowLeft } from 'lucide-svelte';
|
|
9
|
+
import { Button } from './ui/button/index.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
resourceName: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
headerActions?: Snippet;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
resourceName,
|
|
20
|
+
title,
|
|
21
|
+
headerActions,
|
|
22
|
+
class: className = '',
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const resource = getResource(resourceName);
|
|
26
|
+
const pageTitle = title ?? `${t('common.create')}${resource.label}`;
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="space-y-6 {className}">
|
|
30
|
+
<PageHeader title={pageTitle}>
|
|
31
|
+
{#snippet actions()}
|
|
32
|
+
<Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
|
|
33
|
+
<ArrowLeft class="h-5 w-5" />
|
|
34
|
+
</Button>
|
|
35
|
+
{#if headerActions}
|
|
36
|
+
{@render headerActions()}
|
|
37
|
+
{/if}
|
|
38
|
+
{/snippet}
|
|
39
|
+
</PageHeader>
|
|
40
|
+
|
|
41
|
+
<AutoForm {resourceName} mode="create" />
|
|
42
|
+
</div>
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
import { getLocale } from '@svadmin/core/i18n';
|
|
5
5
|
import { currentPath } from '@svadmin/core/router';
|
|
6
6
|
import { Button } from './ui/button/index.js';
|
|
7
|
-
import { X, Bug, ChevronDown, ChevronUp } from 'lucide-svelte';
|
|
7
|
+
import { X, Bug, ChevronDown, ChevronUp, Wand2 } from 'lucide-svelte';
|
|
8
|
+
import InferencerPanel from './InferencerPanel.svelte';
|
|
8
9
|
|
|
9
10
|
let visible = $state(false);
|
|
10
11
|
let collapsed = $state(false);
|
|
12
|
+
let activeTab = $state<'state' | 'inferencer'>('state');
|
|
11
13
|
|
|
12
14
|
function toggle() {
|
|
13
15
|
visible = !visible;
|
|
@@ -53,44 +55,57 @@
|
|
|
53
55
|
</div>
|
|
54
56
|
|
|
55
57
|
{#if !collapsed}
|
|
56
|
-
<div class="devtools-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
<div class="devtools-tabs">
|
|
59
|
+
<button class="devtools-tab" class:active={activeTab === 'state'} onclick={() => activeTab = 'state'}>
|
|
60
|
+
State
|
|
61
|
+
</button>
|
|
62
|
+
<button class="devtools-tab" class:active={activeTab === 'inferencer'} onclick={() => activeTab = 'inferencer'}>
|
|
63
|
+
<Wand2 class="h-3 w-3" /> Inferencer
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<div class="devtools-
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<code class="devtools-value">{colorTheme}</code>
|
|
67
|
+
<div class="devtools-body">
|
|
68
|
+
{#if activeTab === 'state'}
|
|
69
|
+
<div class="devtools-section">
|
|
70
|
+
<h4>Router</h4>
|
|
71
|
+
<div class="devtools-row">
|
|
72
|
+
<span class="devtools-label">Path</span>
|
|
73
|
+
<code class="devtools-value">{path}</code>
|
|
74
|
+
</div>
|
|
74
75
|
</div>
|
|
75
|
-
</div>
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
<div class="devtools-section">
|
|
78
|
+
<h4>Theme</h4>
|
|
79
|
+
<div class="devtools-row">
|
|
80
|
+
<span class="devtools-label">Mode</span>
|
|
81
|
+
<code class="devtools-value">{theme}</code>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="devtools-row">
|
|
84
|
+
<span class="devtools-label">Color</span>
|
|
85
|
+
<code class="devtools-value">{colorTheme}</code>
|
|
86
|
+
</div>
|
|
82
87
|
</div>
|
|
83
|
-
</div>
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{#each resources as r}
|
|
89
|
+
<div class="devtools-section">
|
|
90
|
+
<h4>i18n</h4>
|
|
88
91
|
<div class="devtools-row">
|
|
89
|
-
<span class="devtools-label">
|
|
90
|
-
<code class="devtools-value">{
|
|
92
|
+
<span class="devtools-label">Locale</span>
|
|
93
|
+
<code class="devtools-value">{locale}</code>
|
|
91
94
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="devtools-section">
|
|
98
|
+
<h4>Resources ({resources.length})</h4>
|
|
99
|
+
{#each resources as r}
|
|
100
|
+
<div class="devtools-row">
|
|
101
|
+
<span class="devtools-label">{r.name}</span>
|
|
102
|
+
<code class="devtools-value">{r.fields.length} fields</code>
|
|
103
|
+
</div>
|
|
104
|
+
{/each}
|
|
105
|
+
</div>
|
|
106
|
+
{:else}
|
|
107
|
+
<InferencerPanel />
|
|
108
|
+
{/if}
|
|
94
109
|
</div>
|
|
95
110
|
{/if}
|
|
96
111
|
</div>
|
|
@@ -107,7 +122,8 @@
|
|
|
107
122
|
bottom: 0;
|
|
108
123
|
right: 1rem;
|
|
109
124
|
z-index: 9999;
|
|
110
|
-
width:
|
|
125
|
+
width: 420px;
|
|
126
|
+
max-width: 95vw;
|
|
111
127
|
background: hsl(var(--card));
|
|
112
128
|
border: 1px solid hsl(var(--border));
|
|
113
129
|
border-bottom: none;
|
|
@@ -161,7 +177,7 @@
|
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
.devtools-body {
|
|
164
|
-
max-height:
|
|
180
|
+
max-height: 400px;
|
|
165
181
|
overflow-y: auto;
|
|
166
182
|
padding: 0.5rem;
|
|
167
183
|
}
|
|
@@ -228,4 +244,37 @@
|
|
|
228
244
|
opacity: 1;
|
|
229
245
|
transform: scale(1.1);
|
|
230
246
|
}
|
|
247
|
+
.devtools-tabs {
|
|
248
|
+
display: flex;
|
|
249
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.devtools-tab {
|
|
253
|
+
flex: 1;
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
justify-content: center;
|
|
257
|
+
gap: 0.25rem;
|
|
258
|
+
padding: 0.375rem 0.5rem;
|
|
259
|
+
font-size: 0.6875rem;
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
text-transform: uppercase;
|
|
262
|
+
letter-spacing: 0.05em;
|
|
263
|
+
background: none;
|
|
264
|
+
border: none;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
color: hsl(var(--muted-foreground));
|
|
267
|
+
border-bottom: 2px solid transparent;
|
|
268
|
+
transition: all 0.15s;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.devtools-tab:hover {
|
|
272
|
+
color: hsl(var(--foreground));
|
|
273
|
+
background: hsl(var(--muted) / 0.3);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.devtools-tab.active {
|
|
277
|
+
color: hsl(var(--primary));
|
|
278
|
+
border-bottom-color: hsl(var(--primary));
|
|
279
|
+
}
|
|
231
280
|
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getResource } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import PageHeader from './PageHeader.svelte';
|
|
7
|
+
import AutoForm from './AutoForm.svelte';
|
|
8
|
+
import { ArrowLeft, Trash2 } from 'lucide-svelte';
|
|
9
|
+
import { Button } from './ui/button/index.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
resourceName: string;
|
|
13
|
+
id: string | number;
|
|
14
|
+
title?: string;
|
|
15
|
+
canDelete?: boolean;
|
|
16
|
+
headerActions?: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
resourceName,
|
|
22
|
+
id,
|
|
23
|
+
title,
|
|
24
|
+
canDelete,
|
|
25
|
+
headerActions,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const resource = getResource(resourceName);
|
|
30
|
+
const pageTitle = title ?? `${t('common.edit')} ${resource.label} #${id}`;
|
|
31
|
+
const showDelete = canDelete ?? resource.canDelete !== false;
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="space-y-6 {className}">
|
|
35
|
+
<PageHeader title={pageTitle}>
|
|
36
|
+
{#snippet actions()}
|
|
37
|
+
<Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
|
|
38
|
+
<ArrowLeft class="h-5 w-5" />
|
|
39
|
+
</Button>
|
|
40
|
+
{#if showDelete}
|
|
41
|
+
<Button variant="destructive" size="sm">
|
|
42
|
+
<Trash2 class="h-4 w-4" data-icon="inline-start" /> {t('common.delete')}
|
|
43
|
+
</Button>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if headerActions}
|
|
46
|
+
{@render headerActions()}
|
|
47
|
+
{/if}
|
|
48
|
+
{/snippet}
|
|
49
|
+
</PageHeader>
|
|
50
|
+
|
|
51
|
+
<AutoForm {resourceName} mode="edit" {id} />
|
|
52
|
+
</div>
|
|
@@ -1,41 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import { t } from '@svadmin/core
|
|
4
|
-
import { Button } from './ui/button/index.js';
|
|
5
|
-
import { InboxIcon } from 'lucide-svelte';
|
|
2
|
+
import { PackageOpen } from 'lucide-svelte';
|
|
3
|
+
import { t } from '@svadmin/core';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
let {
|
|
6
|
+
title = t('empty.title'),
|
|
7
|
+
description = t('empty.description'),
|
|
8
|
+
children,
|
|
9
|
+
} = $props<{
|
|
9
10
|
title?: string;
|
|
10
11
|
description?: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let {
|
|
16
|
-
icon: Icon = InboxIcon,
|
|
17
|
-
title,
|
|
18
|
-
description,
|
|
19
|
-
action,
|
|
20
|
-
class: className = '',
|
|
21
|
-
}: Props = $props();
|
|
12
|
+
children?: import('svelte').Snippet;
|
|
13
|
+
}>();
|
|
22
14
|
</script>
|
|
23
15
|
|
|
24
|
-
<div class="flex flex-col items-center justify-center
|
|
25
|
-
<div class="
|
|
26
|
-
<
|
|
16
|
+
<div class="flex flex-col items-center justify-center p-8 text-center min-h-[300px] bg-card rounded-lg border border-dashed border-border/50 shadow-sm transition-all duration-300">
|
|
17
|
+
<div class="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
18
|
+
<PackageOpen class="h-10 w-10 text-muted-foreground" />
|
|
27
19
|
</div>
|
|
28
|
-
<h3 class="text-lg font-semibold
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{description}
|
|
34
|
-
</p>
|
|
35
|
-
{/if}
|
|
36
|
-
{#if action}
|
|
37
|
-
<div class="mt-4">
|
|
38
|
-
{@render action()}
|
|
20
|
+
<h3 class="text-lg font-semibold tracking-tight">{title}</h3>
|
|
21
|
+
<p class="text-sm text-muted-foreground mt-2 max-w-sm">{description}</p>
|
|
22
|
+
{#if children}
|
|
23
|
+
<div class="mt-6">
|
|
24
|
+
{@render children()}
|
|
39
25
|
</div>
|
|
40
26
|
{/if}
|
|
41
27
|
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { AlertTriangle, AlertCircle } from 'lucide-svelte';
|
|
3
|
+
import { t } from '@svadmin/core';
|
|
4
|
+
import { Button } from './ui/button';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
error,
|
|
8
|
+
title = t('error.title'),
|
|
9
|
+
description = t('error.description'),
|
|
10
|
+
retry,
|
|
11
|
+
} = $props<{
|
|
12
|
+
error?: Error | null;
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
retry?: () => void;
|
|
16
|
+
}>();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-col items-center justify-center p-8 text-center min-h-[300px] bg-destructive/5 rounded-lg border border-destructive/20 text-destructive transition-all duration-300">
|
|
20
|
+
<div class="h-16 w-16 rounded-full bg-destructive/10 flex items-center justify-center mb-4">
|
|
21
|
+
<AlertTriangle class="h-8 w-8 text-destructive" />
|
|
22
|
+
</div>
|
|
23
|
+
<h3 class="text-lg font-semibold">{title}</h3>
|
|
24
|
+
<p class="text-sm opacity-90 mt-2 max-w-md">
|
|
25
|
+
{error?.message || description}
|
|
26
|
+
</p>
|
|
27
|
+
{#if retry}
|
|
28
|
+
<div class="mt-6">
|
|
29
|
+
<Button variant="outline" class="border-destructive/30 hover:bg-destructive/10 text-destructive" onclick={retry}>
|
|
30
|
+
<AlertCircle class="mr-2 h-4 w-4" />
|
|
31
|
+
{t('buttons.retry')}
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
{/if}
|
|
35
|
+
</div>
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import { useForgotPassword } from '@svadmin/core';
|
|
3
3
|
import { t } from '@svadmin/core/i18n';
|
|
4
4
|
import { navigate } from '@svadmin/core/router';
|
|
5
|
-
import { toast } from '@svadmin/core/toast';
|
|
6
5
|
import { Button } from './ui/button/index.js';
|
|
7
6
|
import { Input } from './ui/input/index.js';
|
|
8
7
|
import * as Card from './ui/card/index.js';
|
|
9
8
|
import { KeyRound, Mail, ArrowLeft, CheckCircle } from 'lucide-svelte';
|
|
10
9
|
|
|
11
|
-
let {
|
|
12
|
-
authProvider: AuthProvider;
|
|
10
|
+
let { title = 'Admin' } = $props<{
|
|
13
11
|
title?: string;
|
|
14
12
|
}>();
|
|
15
13
|
|
|
14
|
+
const forgot = useForgotPassword();
|
|
15
|
+
|
|
16
16
|
let email = $state('');
|
|
17
|
-
let loading = $state(false);
|
|
18
17
|
let error = $state('');
|
|
19
18
|
let sent = $state(false);
|
|
20
19
|
|
|
@@ -24,20 +23,11 @@
|
|
|
24
23
|
|
|
25
24
|
if (!email) { error = t('auth.emailRequired'); return; }
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
26
|
+
const result = await forgot.mutate({ email });
|
|
27
|
+
if (result.success) {
|
|
28
|
+
sent = true;
|
|
29
|
+
} else {
|
|
30
|
+
error = result.error?.message ?? t('common.operationFailed');
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
33
|
</script>
|
|
@@ -94,8 +84,8 @@
|
|
|
94
84
|
</div>
|
|
95
85
|
</div>
|
|
96
86
|
|
|
97
|
-
<Button type="submit" class="w-full" disabled={
|
|
98
|
-
{#if
|
|
87
|
+
<Button type="submit" class="w-full" disabled={forgot.isLoading}>
|
|
88
|
+
{#if forgot.isLoading}
|
|
99
89
|
<span class="loading-spinner"></span>
|
|
100
90
|
{/if}
|
|
101
91
|
{t('auth.sendResetLink')}
|
|
@@ -164,7 +154,7 @@
|
|
|
164
154
|
position: relative;
|
|
165
155
|
}
|
|
166
156
|
|
|
167
|
-
.input-icon {
|
|
157
|
+
:global(.input-icon) {
|
|
168
158
|
position: absolute;
|
|
169
159
|
left: 0.75rem;
|
|
170
160
|
top: 50%;
|