@svadmin/ui 0.0.3 → 0.0.6
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 +2 -2
- package/src/components/AdminApp.svelte +27 -18
- package/src/components/Authenticated.svelte +26 -0
- package/src/components/ConfigErrorScreen.svelte +224 -0
- package/src/components/DevTools.svelte +83 -34
- package/src/components/ForgotPasswordPage.svelte +12 -22
- package/src/components/InferencerPanel.svelte +166 -0
- package/src/components/LoginPage.svelte +13 -22
- package/src/components/RegisterPage.svelte +12 -23
- package/src/components/StatsCard.svelte +23 -2
- 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 +60 -0
- package/src/components/buttons/EditButton.svelte +30 -0
- package/src/components/buttons/ExportButton.svelte +24 -0
- package/src/components/buttons/ImportButton.svelte +28 -0
- package/src/components/buttons/ListButton.svelte +23 -0
- package/src/components/buttons/RefreshButton.svelte +30 -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/index.ts +10 -0
- package/src/router-state.svelte.ts +24 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"svelte": "^5.0.0",
|
|
24
|
-
"@svadmin/core": "0.
|
|
24
|
+
"@svadmin/core": "0.0.5",
|
|
25
25
|
"bits-ui": "^2.0.0",
|
|
26
26
|
"tailwind-variants": "^3.0.0",
|
|
27
27
|
"clsx": "^2.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode } from '@svadmin/core';
|
|
4
|
-
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme } from '@svadmin/core';
|
|
3
|
+
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider } from '@svadmin/core';
|
|
4
|
+
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider } from '@svadmin/core';
|
|
5
5
|
import { navigate } from '@svadmin/core/router';
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
7
7
|
import Layout from './Layout.svelte';
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
import LoginPage from './LoginPage.svelte';
|
|
13
13
|
import RegisterPage from './RegisterPage.svelte';
|
|
14
14
|
import ForgotPasswordPage from './ForgotPasswordPage.svelte';
|
|
15
|
+
import UpdatePasswordPage from './UpdatePasswordPage.svelte';
|
|
16
|
+
import ConfigErrorScreen from './ConfigErrorScreen.svelte';
|
|
15
17
|
import DevTools from './DevTools.svelte';
|
|
16
18
|
import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
|
|
17
19
|
|
|
18
20
|
interface Props {
|
|
19
21
|
dataProvider: DataProvider;
|
|
20
22
|
authProvider?: AuthProvider;
|
|
23
|
+
routerProvider?: RouterProvider;
|
|
21
24
|
resources: ResourceDefinition[];
|
|
22
25
|
locale?: string;
|
|
23
26
|
title?: string;
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
let {
|
|
30
33
|
dataProvider,
|
|
31
34
|
authProvider,
|
|
35
|
+
routerProvider,
|
|
32
36
|
resources,
|
|
33
37
|
locale,
|
|
34
38
|
title = 'Admin',
|
|
@@ -37,10 +41,14 @@
|
|
|
37
41
|
loginPage,
|
|
38
42
|
}: Props = $props();
|
|
39
43
|
|
|
44
|
+
// Resolve router provider (default to hash)
|
|
45
|
+
const resolvedRouter = routerProvider ?? createHashRouterProvider();
|
|
46
|
+
|
|
40
47
|
// Set up context
|
|
41
48
|
setDataProvider(dataProvider);
|
|
42
49
|
if (authProvider) setAuthProvider(authProvider);
|
|
43
50
|
setResources(resources);
|
|
51
|
+
setRouterProvider(resolvedRouter);
|
|
44
52
|
if (locale) setLocale(locale);
|
|
45
53
|
if (defaultTheme) setTheme(defaultTheme);
|
|
46
54
|
|
|
@@ -50,23 +58,27 @@
|
|
|
50
58
|
},
|
|
51
59
|
});
|
|
52
60
|
|
|
53
|
-
// Initialize
|
|
54
|
-
initRouter();
|
|
61
|
+
// Initialize router with provider
|
|
62
|
+
initRouter(resolvedRouter);
|
|
55
63
|
|
|
56
64
|
// Reactive getters for route state
|
|
57
65
|
const route = $derived(getRoute());
|
|
58
66
|
const params = $derived(getParams());
|
|
59
67
|
|
|
60
68
|
// Auth check
|
|
61
|
-
let isAuthenticated = $state(
|
|
62
|
-
let authChecked = $state(
|
|
69
|
+
let isAuthenticated = $state(false);
|
|
70
|
+
let authChecked = $state(false);
|
|
63
71
|
|
|
64
72
|
$effect(() => {
|
|
65
|
-
if (!authProvider)
|
|
73
|
+
if (!authProvider) {
|
|
74
|
+
isAuthenticated = true;
|
|
75
|
+
authChecked = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
66
78
|
authProvider.check().then(result => {
|
|
67
79
|
isAuthenticated = result.authenticated;
|
|
68
80
|
authChecked = true;
|
|
69
|
-
if (!result.authenticated && route !== '/login' && route !== '/register' && route !== '/forgot-password') {
|
|
81
|
+
if (!result.authenticated && route !== '/login' && route !== '/register' && route !== '/forgot-password' && route !== '/update-password') {
|
|
70
82
|
navigate(result.redirectTo ?? '/login');
|
|
71
83
|
}
|
|
72
84
|
});
|
|
@@ -81,18 +93,15 @@
|
|
|
81
93
|
{:else if route === '/login' && loginPage}
|
|
82
94
|
{@render loginPage()}
|
|
83
95
|
{:else if route === '/login' && authProvider}
|
|
84
|
-
<LoginPage {
|
|
96
|
+
<LoginPage {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
|
|
85
97
|
{:else if route === '/register' && authProvider?.register}
|
|
86
|
-
<RegisterPage {
|
|
98
|
+
<RegisterPage {title} />
|
|
87
99
|
{:else if route === '/forgot-password' && authProvider?.forgotPassword}
|
|
88
|
-
<ForgotPasswordPage {
|
|
89
|
-
{:else if route === '/
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<p class="mt-2 text-sm text-gray-500">Please configure an authProvider.</p>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
100
|
+
<ForgotPasswordPage {title} />
|
|
101
|
+
{:else if route === '/update-password' && authProvider?.updatePassword}
|
|
102
|
+
<UpdatePasswordPage {title} />
|
|
103
|
+
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
104
|
+
<ConfigErrorScreen title="{title} — Configuration Required" />
|
|
96
105
|
{:else if isAuthenticated || !authProvider}
|
|
97
106
|
<Layout {title}>
|
|
98
107
|
{#if route === '/'}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { useIsAuthenticated } from '@svadmin/core';
|
|
4
|
+
|
|
5
|
+
let { children, fallback, loading } = $props<{
|
|
6
|
+
children: Snippet;
|
|
7
|
+
fallback?: Snippet;
|
|
8
|
+
loading?: Snippet;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const auth = useIsAuthenticated();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
{#if auth.isLoading}
|
|
15
|
+
{#if loading}
|
|
16
|
+
{@render loading()}
|
|
17
|
+
{:else}
|
|
18
|
+
<div class="flex items-center justify-center min-h-[200px]">
|
|
19
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
20
|
+
</div>
|
|
21
|
+
{/if}
|
|
22
|
+
{:else if auth.isAuthenticated}
|
|
23
|
+
{@render children()}
|
|
24
|
+
{:else if fallback}
|
|
25
|
+
{@render fallback()}
|
|
26
|
+
{/if}
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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%;
|