@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.
Files changed (54) hide show
  1. package/package.json +3 -2
  2. package/src/components/AdminApp.svelte +31 -21
  3. package/src/components/Authenticated.svelte +57 -0
  4. package/src/components/AutoForm.svelte +5 -5
  5. package/src/components/AutoSaveIndicator.svelte +30 -0
  6. package/src/components/AutoTable.svelte +81 -10
  7. package/src/components/Breadcrumbs.svelte +2 -0
  8. package/src/components/CatchAllNavigate.svelte +21 -0
  9. package/src/components/ConfigErrorScreen.svelte +224 -0
  10. package/src/components/ConfirmDialog.svelte +1 -1
  11. package/src/components/CreatePage.svelte +42 -0
  12. package/src/components/DevTools.svelte +83 -34
  13. package/src/components/EditPage.svelte +52 -0
  14. package/src/components/EmptyState.svelte +17 -31
  15. package/src/components/ErrorComponent.svelte +35 -0
  16. package/src/components/ForgotPasswordPage.svelte +12 -22
  17. package/src/components/Header.svelte +28 -59
  18. package/src/components/InferencerPanel.svelte +166 -0
  19. package/src/components/ListPage.svelte +47 -0
  20. package/src/components/LiveIndicator.svelte +115 -0
  21. package/src/components/LoginPage.svelte +13 -22
  22. package/src/components/NavigateToResource.svelte +20 -0
  23. package/src/components/PageSkeleton.svelte +70 -0
  24. package/src/components/RegisterPage.svelte +12 -23
  25. package/src/components/ShowPage.svelte +3 -3
  26. package/src/components/Sidebar.svelte +29 -10
  27. package/src/components/StatsCard.svelte +23 -2
  28. package/src/components/ThemedTitle.svelte +32 -0
  29. package/src/components/Toast.svelte +21 -11
  30. package/src/components/UpdatePasswordPage.svelte +217 -0
  31. package/src/components/buttons/CloneButton.svelte +30 -0
  32. package/src/components/buttons/CreateButton.svelte +29 -0
  33. package/src/components/buttons/DeleteButton.svelte +59 -0
  34. package/src/components/buttons/EditButton.svelte +30 -0
  35. package/src/components/buttons/ExportButton.svelte +24 -0
  36. package/src/components/buttons/ImportButton.svelte +50 -0
  37. package/src/components/buttons/ListButton.svelte +23 -0
  38. package/src/components/buttons/RefreshButton.svelte +31 -0
  39. package/src/components/buttons/SaveButton.svelte +27 -0
  40. package/src/components/buttons/ShowButton.svelte +24 -0
  41. package/src/components/buttons/index.ts +10 -0
  42. package/src/components/fields/BooleanField.svelte +13 -0
  43. package/src/components/fields/DateField.svelte +6 -0
  44. package/src/components/fields/EmailField.svelte +15 -0
  45. package/src/components/fields/FileField.svelte +19 -0
  46. package/src/components/fields/ImageField.svelte +14 -0
  47. package/src/components/fields/MarkdownField.svelte +19 -0
  48. package/src/components/fields/NumberField.svelte +6 -0
  49. package/src/components/fields/TagField.svelte +17 -0
  50. package/src/components/fields/TextField.svelte +16 -0
  51. package/src/components/fields/UrlField.svelte +20 -0
  52. package/src/components/ui/avatar/avatar.svelte +2 -1
  53. package/src/index.ts +32 -0
  54. 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-body">
57
- <div class="devtools-section">
58
- <h4>Router</h4>
59
- <div class="devtools-row">
60
- <span class="devtools-label">Path</span>
61
- <code class="devtools-value">{path}</code>
62
- </div>
63
- </div>
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
- <div class="devtools-section">
66
- <h4>Theme</h4>
67
- <div class="devtools-row">
68
- <span class="devtools-label">Mode</span>
69
- <code class="devtools-value">{theme}</code>
70
- </div>
71
- <div class="devtools-row">
72
- <span class="devtools-label">Color</span>
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
- <div class="devtools-section">
78
- <h4>i18n</h4>
79
- <div class="devtools-row">
80
- <span class="devtools-label">Locale</span>
81
- <code class="devtools-value">{locale}</code>
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
- <div class="devtools-section">
86
- <h4>Resources ({resources.length})</h4>
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">{r.name}</span>
90
- <code class="devtools-value">{r.fields.length} fields</code>
92
+ <span class="devtools-label">Locale</span>
93
+ <code class="devtools-value">{locale}</code>
91
94
  </div>
92
- {/each}
93
- </div>
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: 320px;
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: 300px;
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 type { Snippet } from 'svelte';
3
- import { t } from '@svadmin/core/i18n';
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
- interface Props {
8
- icon?: typeof InboxIcon;
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
- action?: Snippet;
12
- class?: string;
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 py-16 text-center {className}">
25
- <div class="flex h-16 w-16 items-center justify-center rounded-full bg-muted mb-4">
26
- <Icon class="h-8 w-8 text-muted-foreground" />
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 text-foreground">
29
- {title ?? t('common.noData')}
30
- </h3>
31
- {#if description}
32
- <p class="mt-1 text-sm text-muted-foreground max-w-sm">
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 type { AuthProvider } from '@svadmin/core';
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 { authProvider, title = 'Admin' } = $props<{
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
- 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;
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={loading}>
98
- {#if loading}
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%;