@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
@@ -1,66 +1,35 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { Identity } from '@svadmin/core';
4
- import { toggleTheme, getResolvedTheme } from '@svadmin/core';
5
- import { t } from '@svadmin/core/i18n';
6
- import { Button } from './ui/button/index.js';
7
- import { Sun, Moon, Bell, Search } from 'lucide-svelte';
8
-
9
- interface Props {
10
- identity?: Identity | null;
11
- title?: string;
12
- showSearch?: boolean;
13
- actions?: Snippet;
14
- class?: string;
15
- }
2
+ import { Moon, Sun } from 'lucide-svelte';
3
+ import { Button } from './ui/button';
4
+ import { getTheme, toggleTheme } from '@svadmin/core';
16
5
 
17
6
  let {
18
- identity,
19
- title = '',
20
- showSearch = false,
21
- actions,
22
- class: className = '',
23
- }: Props = $props();
7
+ showThemeToggle = true,
8
+ children,
9
+ } = $props<{
10
+ showThemeToggle?: boolean;
11
+ children?: import('svelte').Snippet;
12
+ }>();
24
13
  </script>
25
14
 
26
- <header class="sticky top-0 z-20 flex h-14 items-center gap-4 border-b border-border bg-background/95 px-6 backdrop-blur supports-[backdrop-filter]:bg-background/60 {className}">
27
- {#if title}
28
- <h2 class="text-lg font-semibold text-foreground">{title}</h2>
29
- {/if}
30
-
31
- <div class="flex-1"></div>
32
-
33
- {#if showSearch}
34
- <div class="relative hidden md:block">
35
- <Search class="absolute left-2.5 top-2 h-4 w-4 text-muted-foreground" />
36
- <input
37
- type="search"
38
- placeholder={t('common.search')}
39
- class="h-8 w-64 rounded-lg border border-input bg-background pl-8 pr-3 text-sm outline-none focus:border-ring focus:ring-1 focus:ring-ring"
40
- />
41
- </div>
42
- {/if}
43
-
44
- {#if actions}
45
- {@render actions()}
46
- {/if}
47
-
48
- <!-- Theme toggle -->
49
- <Button variant="ghost" size="icon-sm" onclick={toggleTheme} title={t('common.toggleTheme')}>
50
- {#if getResolvedTheme() === 'dark'}
51
- <Sun class="h-4 w-4" />
52
- {:else}
53
- <Moon class="h-4 w-4" />
15
+ <header class="sticky top-0 z-30 flex h-16 w-full shrink-0 items-center justify-between border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-6">
16
+ <div class="flex items-center gap-4">
17
+ <!-- Mobile sidebar toggle can go here -->
18
+ </div>
19
+ <div class="ml-auto flex items-center gap-4">
20
+ {#if children}
21
+ {@render children()}
54
22
  {/if}
55
- </Button>
56
-
57
- <!-- User info -->
58
- {#if identity}
59
- <div class="flex items-center gap-2">
60
- <div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
61
- {identity.name?.charAt(0).toUpperCase() ?? 'U'}
62
- </div>
63
- <span class="hidden text-sm font-medium text-foreground md:inline">{identity.name}</span>
64
- </div>
65
- {/if}
23
+
24
+ {#if showThemeToggle}
25
+ <Button variant="ghost" size="icon" onclick={() => toggleTheme()} class="rounded-full">
26
+ {#if getTheme() === 'dark'}
27
+ <Moon class="h-5 w-5 transition-all" />
28
+ {:else}
29
+ <Sun class="h-5 w-5 transition-all" />
30
+ {/if}
31
+ <span class="sr-only">Toggle theme</span>
32
+ </Button>
33
+ {/if}
34
+ </div>
66
35
  </header>
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import { getDataProvider, getResources, inferResource } from '@svadmin/core';
3
+ import type { InferResult, ResourceDefinition } from '@svadmin/core';
4
+ import { Button } from './ui/button/index.js';
5
+ import { Wand2, Copy, Check, RefreshCw, Loader2 } from 'lucide-svelte';
6
+
7
+ const dataProvider = getDataProvider();
8
+ const resources = getResources();
9
+
10
+ let selectedResource = $state('');
11
+ let inferResult = $state<InferResult | null>(null);
12
+ let loading = $state(false);
13
+ let error = $state<string | null>(null);
14
+ let copied = $state(false);
15
+ let customEndpoint = $state('');
16
+
17
+ async function runInference() {
18
+ const resourceName = customEndpoint.trim() || selectedResource;
19
+ if (!resourceName) return;
20
+
21
+ loading = true;
22
+ error = null;
23
+ inferResult = null;
24
+
25
+ try {
26
+ const response = await dataProvider.getList({
27
+ resource: resourceName,
28
+ pagination: { current: 1, pageSize: 25 },
29
+ });
30
+
31
+ if (!response.data || response.data.length === 0) {
32
+ error = `No data returned from "${resourceName}". The API must return at least one record to infer fields.`;
33
+ return;
34
+ }
35
+
36
+ inferResult = inferResource(
37
+ resourceName,
38
+ response.data as Record<string, unknown>[],
39
+ );
40
+ } catch (e: any) {
41
+ error = e?.message ?? 'Failed to fetch data for inference.';
42
+ } finally {
43
+ loading = false;
44
+ }
45
+ }
46
+
47
+ function copyCode() {
48
+ if (!inferResult) return;
49
+ navigator.clipboard.writeText(inferResult.code);
50
+ copied = true;
51
+ setTimeout(() => { copied = false; }, 2000);
52
+ }
53
+
54
+ const typeColors: Record<string, string> = {
55
+ text: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
56
+ number: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
57
+ boolean: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
58
+ date: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
59
+ email: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-200',
60
+ url: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200',
61
+ image: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200',
62
+ images: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200',
63
+ textarea: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
64
+ json: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
65
+ tags: 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200',
66
+ select: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
67
+ relation: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
68
+ color: 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-900 dark:text-fuchsia-200',
69
+ phone: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200',
70
+ };
71
+ </script>
72
+
73
+ <div class="inferencer-panel space-y-4">
74
+ <div class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
75
+ <Wand2 class="h-4 w-4" />
76
+ Resource Inferencer
77
+ </div>
78
+
79
+ <!-- Resource selector -->
80
+ <div class="flex gap-2">
81
+ <select
82
+ class="flex-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
83
+ bind:value={selectedResource}
84
+ >
85
+ <option value="">— Select a resource —</option>
86
+ {#each resources as res}
87
+ <option value={res.name}>{res.label} ({res.name})</option>
88
+ {/each}
89
+ </select>
90
+ <span class="flex items-center text-xs text-gray-400">or</span>
91
+ <input
92
+ type="text"
93
+ class="w-36 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
94
+ placeholder="custom endpoint"
95
+ bind:value={customEndpoint}
96
+ />
97
+ </div>
98
+
99
+ <Button size="sm" onclick={runInference} disabled={loading || (!selectedResource && !customEndpoint.trim())}>
100
+ {#if loading}
101
+ <Loader2 class="mr-1 h-3 w-3 animate-spin" />
102
+ Inferring...
103
+ {:else}
104
+ <RefreshCw class="mr-1 h-3 w-3" />
105
+ Infer Fields
106
+ {/if}
107
+ </Button>
108
+
109
+ {#if error}
110
+ <div class="rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-300">
111
+ {error}
112
+ </div>
113
+ {/if}
114
+
115
+ {#if inferResult}
116
+ <!-- Field table -->
117
+ <div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
118
+ <table class="w-full text-left text-sm">
119
+ <thead class="bg-gray-50 dark:bg-gray-800">
120
+ <tr>
121
+ <th class="px-3 py-2 font-medium">Field</th>
122
+ <th class="px-3 py-2 font-medium">Type</th>
123
+ <th class="px-3 py-2 font-medium">List</th>
124
+ <th class="px-3 py-2 font-medium">Form</th>
125
+ </tr>
126
+ </thead>
127
+ <tbody>
128
+ {#each inferResult.fields as field}
129
+ <tr class="border-t border-gray-100 dark:border-gray-700/50">
130
+ <td class="px-3 py-1.5 font-mono text-xs">{field.key}</td>
131
+ <td class="px-3 py-1.5">
132
+ <span class="inline-block rounded-full px-2 py-0.5 text-xs font-medium {typeColors[field.type] ?? 'bg-gray-100 text-gray-600'}">
133
+ {field.type}
134
+ {#if field.resource}→ {field.resource}{/if}
135
+ </span>
136
+ </td>
137
+ <td class="px-3 py-1.5 text-center">{field.showInList ? '✓' : '—'}</td>
138
+ <td class="px-3 py-1.5 text-center">{field.showInForm ? '✓' : '—'}</td>
139
+ </tr>
140
+ {/each}
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+
145
+ <!-- Generated code -->
146
+ <div class="relative">
147
+ <div class="flex items-center justify-between rounded-t-lg bg-gray-800 px-3 py-1.5 text-xs text-gray-400">
148
+ <span>Generated ResourceDefinition</span>
149
+ <button class="flex items-center gap-1 hover:text-white transition-colors" onclick={copyCode}>
150
+ {#if copied}
151
+ <Check class="h-3 w-3 text-green-400" />
152
+ <span class="text-green-400">Copied!</span>
153
+ {:else}
154
+ <Copy class="h-3 w-3" />
155
+ Copy
156
+ {/if}
157
+ </button>
158
+ </div>
159
+ <pre class="max-h-64 overflow-auto rounded-b-lg bg-gray-900 p-3 text-xs text-green-300 font-mono">{inferResult.code}</pre>
160
+ </div>
161
+
162
+ <p class="text-xs text-gray-400">
163
+ Inferred {inferResult.fields.length} fields from sample data. Copy the code above into your <code>resources.ts</code> file.
164
+ </p>
165
+ {/if}
166
+ </div>
@@ -0,0 +1,47 @@
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 AutoTable from './AutoTable.svelte';
8
+ import { Button } from './ui/button/index.js';
9
+ import { Plus } from 'lucide-svelte';
10
+
11
+ interface Props {
12
+ resourceName: string;
13
+ title?: string;
14
+ canCreate?: boolean;
15
+ headerActions?: Snippet;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ resourceName,
21
+ title,
22
+ canCreate,
23
+ headerActions,
24
+ class: className = '',
25
+ }: Props = $props();
26
+
27
+ const resource = getResource(resourceName);
28
+ const pageTitle = title ?? resource.label;
29
+ const showCreate = canCreate ?? resource.canCreate !== false;
30
+ </script>
31
+
32
+ <div class="space-y-6 {className}">
33
+ <PageHeader title={pageTitle}>
34
+ {#snippet actions()}
35
+ {#if showCreate}
36
+ <Button onclick={() => navigate(`/${resourceName}/create`)}>
37
+ <Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
38
+ </Button>
39
+ {/if}
40
+ {#if headerActions}
41
+ {@render headerActions()}
42
+ {/if}
43
+ {/snippet}
44
+ </PageHeader>
45
+
46
+ <AutoTable {resourceName} />
47
+ </div>
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ import { Wifi, WifiOff, Loader2 } from 'lucide-svelte';
3
+
4
+ let {
5
+ status = 'disconnected',
6
+ resource = '',
7
+ lastEvent = null,
8
+ showDetails = false,
9
+ } = $props<{
10
+ status: 'connecting' | 'connected' | 'disconnected';
11
+ resource?: string;
12
+ lastEvent?: { type: string; timestamp?: number } | null;
13
+ showDetails?: boolean;
14
+ }>();
15
+
16
+ const statusConfigs = {
17
+ connected: { color: 'var(--live-connected, #22c55e)', label: 'Live', icon: Wifi },
18
+ connecting: { color: 'var(--live-connecting, #f59e0b)', label: 'Connecting...', icon: Loader2 as typeof Wifi },
19
+ disconnected: { color: 'var(--live-disconnected, #ef4444)', label: 'Offline', icon: WifiOff },
20
+ } as const;
21
+ type StatusKey = 'connected' | 'connecting' | 'disconnected';
22
+ const statusConfig = $derived(statusConfigs[status as StatusKey]);
23
+
24
+ const timeSinceEvent = $derived(
25
+ lastEvent?.timestamp
26
+ ? Math.floor((Date.now() - lastEvent.timestamp) / 1000)
27
+ : null
28
+ );
29
+ </script>
30
+
31
+ <div class="live-indicator" title={statusConfig.label}>
32
+ <span class="live-dot" style="--dot-color: {statusConfig.color}">
33
+ {#if status === 'connected'}
34
+ <span class="live-pulse"></span>
35
+ {/if}
36
+ </span>
37
+ <span class="live-label" style="color: {statusConfig.color}">
38
+ {statusConfig.label}
39
+ </span>
40
+
41
+ {#if showDetails && resource}
42
+ <span class="live-resource">{resource}</span>
43
+ {/if}
44
+
45
+ {#if showDetails && lastEvent}
46
+ <span class="live-event">
47
+ {lastEvent.type}
48
+ {#if timeSinceEvent !== null && timeSinceEvent < 60}
49
+ <span class="live-time">{timeSinceEvent}s ago</span>
50
+ {/if}
51
+ </span>
52
+ {/if}
53
+ </div>
54
+
55
+ <style>
56
+ .live-indicator {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 0.375rem;
60
+ font-size: 0.75rem;
61
+ font-weight: 500;
62
+ padding: 0.25rem 0.5rem;
63
+ border-radius: 9999px;
64
+ background: hsl(var(--muted) / 0.3);
65
+ user-select: none;
66
+ }
67
+
68
+ .live-dot {
69
+ position: relative;
70
+ width: 8px;
71
+ height: 8px;
72
+ border-radius: 50%;
73
+ background: var(--dot-color);
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .live-pulse {
78
+ position: absolute;
79
+ inset: -3px;
80
+ border-radius: 50%;
81
+ background: var(--dot-color);
82
+ opacity: 0.4;
83
+ animation: live-pulse 2s ease-in-out infinite;
84
+ }
85
+
86
+ @keyframes live-pulse {
87
+ 0%, 100% { transform: scale(1); opacity: 0.4; }
88
+ 50% { transform: scale(1.8); opacity: 0; }
89
+ }
90
+
91
+ .live-label {
92
+ font-size: 0.6875rem;
93
+ font-weight: 600;
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.05em;
96
+ }
97
+
98
+ .live-resource {
99
+ font-size: 0.625rem;
100
+ color: hsl(var(--muted-foreground));
101
+ font-family: monospace;
102
+ }
103
+
104
+ .live-event {
105
+ font-size: 0.625rem;
106
+ color: hsl(var(--muted-foreground));
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.25rem;
110
+ }
111
+
112
+ .live-time {
113
+ opacity: 0.6;
114
+ }
115
+ </style>
@@ -1,22 +1,22 @@
1
1
  <script lang="ts">
2
- import type { AuthProvider } from '@svadmin/core';
2
+ import { useLogin, getAuthProvider } 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 { LogIn, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
10
9
 
11
- let { authProvider, title = 'Admin', onSuccess } = $props<{
12
- authProvider: AuthProvider;
10
+ let { title = 'Admin', onSuccess } = $props<{
13
11
  title?: string;
14
12
  onSuccess?: () => void;
15
13
  }>();
16
14
 
15
+ const login = useLogin();
16
+ const authProvider = getAuthProvider();
17
+
17
18
  let email = $state('');
18
19
  let password = $state('');
19
- let loading = $state(false);
20
20
  let showPassword = $state(false);
21
21
  let error = $state('');
22
22
 
@@ -27,20 +27,11 @@
27
27
  if (!email) { error = t('auth.emailRequired'); return; }
28
28
  if (!password) { error = t('auth.passwordRequired'); return; }
29
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;
30
+ const result = await login.mutate({ email, password });
31
+ if (result.success) {
32
+ onSuccess?.();
33
+ } else {
34
+ error = result.error?.message ?? t('common.loginFailed');
44
35
  }
45
36
  }
46
37
  </script>
@@ -114,8 +105,8 @@
114
105
  </div>
115
106
  </div>
116
107
 
117
- <Button type="submit" class="w-full" disabled={loading}>
118
- {#if loading}
108
+ <Button type="submit" class="w-full" disabled={login.isLoading}>
109
+ {#if login.isLoading}
119
110
  <span class="loading-spinner"></span>
120
111
  {/if}
121
112
  {t('auth.loginButton')}
@@ -183,7 +174,7 @@
183
174
  position: relative;
184
175
  }
185
176
 
186
- .input-icon {
177
+ :global(.input-icon) {
187
178
  position: absolute;
188
179
  left: 0.75rem;
189
180
  top: 50%;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { useMenu } from '@svadmin/core';
3
+ import { navigate } from '@svadmin/core/router';
4
+
5
+ const { menuItems } = useMenu();
6
+
7
+ $effect(() => {
8
+ // Automatically navigate to the first defined resource
9
+ if (menuItems.length > 0) {
10
+ navigate(menuItems[0].route);
11
+ }
12
+ });
13
+ </script>
14
+
15
+ <div class="flex h-screen w-screen items-center justify-center p-4 text-muted-foreground">
16
+ <div class="text-center">
17
+ <div class="mb-4 inline-block h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"></div>
18
+ <p>Navigating to default resource...</p>
19
+ </div>
20
+ </div>
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { Skeleton } from './ui/skeleton';
3
+
4
+ let {
5
+ type = 'list',
6
+ rows = 5,
7
+ } = $props<{
8
+ type?: 'list' | 'show' | 'form';
9
+ rows?: number;
10
+ }>();
11
+ </script>
12
+
13
+ <div class="w-full space-y-6">
14
+ <!-- Header Skeleton -->
15
+ <div class="flex items-center justify-between">
16
+ <div class="space-y-2">
17
+ <Skeleton class="h-8 w-[200px]" />
18
+ <Skeleton class="h-4 w-[300px]" />
19
+ </div>
20
+ {#if type === 'list'}
21
+ <Skeleton class="h-10 w-[120px]" />
22
+ {/if}
23
+ </div>
24
+
25
+ <div class="rounded-md border p-4 space-y-4">
26
+ {#if type === 'list'}
27
+ <!-- Table Header -->
28
+ <div class="flex gap-4 border-b pb-4">
29
+ {#each Array(4) as _}
30
+ <Skeleton class="h-6 flex-1" />
31
+ {/each}
32
+ </div>
33
+ <!-- Table Rows -->
34
+ <div class="space-y-4">
35
+ {#each Array(rows) as _}
36
+ <div class="flex gap-4">
37
+ <Skeleton class="h-6 flex-1" />
38
+ <Skeleton class="h-6 flex-1" />
39
+ <Skeleton class="h-6 flex-1" />
40
+ <Skeleton class="h-6 flex-1" />
41
+ </div>
42
+ {/each}
43
+ </div>
44
+ {:else if type === 'show'}
45
+ <!-- Key-Value pairs -->
46
+ <div class="space-y-6">
47
+ {#each Array(rows) as _}
48
+ <div class="space-y-2">
49
+ <Skeleton class="h-4 w-[100px]" />
50
+ <Skeleton class="h-6 w-[250px]" />
51
+ </div>
52
+ {/each}
53
+ </div>
54
+ {:else if type === 'form'}
55
+ <!-- Form Inputs -->
56
+ <div class="space-y-6">
57
+ {#each Array(rows) as _}
58
+ <div class="space-y-2">
59
+ <Skeleton class="h-4 w-[100px]" />
60
+ <Skeleton class="h-10 w-full max-w-md" />
61
+ </div>
62
+ {/each}
63
+ <div class="pt-4 flex gap-2">
64
+ <Skeleton class="h-10 w-[100px]" />
65
+ <Skeleton class="h-10 w-[100px]" />
66
+ </div>
67
+ </div>
68
+ {/if}
69
+ </div>
70
+ </div>
@@ -1,23 +1,22 @@
1
1
  <script lang="ts">
2
- import type { AuthProvider } from '@svadmin/core';
2
+ import { useRegister } 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 { UserPlus, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
10
9
 
11
- let { authProvider, title = 'Admin', onSuccess } = $props<{
12
- authProvider: AuthProvider;
10
+ let { title = 'Admin', onSuccess } = $props<{
13
11
  title?: string;
14
12
  onSuccess?: () => void;
15
13
  }>();
16
14
 
15
+ const register = useRegister();
16
+
17
17
  let email = $state('');
18
18
  let password = $state('');
19
19
  let confirmPassword = $state('');
20
- let loading = $state(false);
21
20
  let showPassword = $state(false);
22
21
  let error = $state('');
23
22
 
@@ -29,21 +28,11 @@
29
28
  if (!password) { error = t('auth.passwordRequired'); return; }
30
29
  if (password !== confirmPassword) { error = t('auth.passwordMismatch'); return; }
31
30
 
32
- loading = true;
33
- try {
34
- const result = await authProvider.register!({ email, password });
35
- if (result.success) {
36
- toast.success(t('auth.registerSuccess'));
37
- onSuccess?.();
38
- navigate(result.redirectTo ?? '/login');
39
- } else {
40
- error = result.error?.message ?? t('common.operationFailed');
41
- }
42
- } catch (err) {
43
- error = err instanceof Error ? err.message : t('common.operationFailed');
44
- toast.error(error);
45
- } finally {
46
- loading = false;
31
+ const result = await register.mutate({ email, password });
32
+ if (result.success) {
33
+ onSuccess?.();
34
+ } else {
35
+ error = result.error?.message ?? t('common.operationFailed');
47
36
  }
48
37
  }
49
38
  </script>
@@ -123,8 +112,8 @@
123
112
  </div>
124
113
  </div>
125
114
 
126
- <Button type="submit" class="w-full" disabled={loading}>
127
- {#if loading}
115
+ <Button type="submit" class="w-full" disabled={register.isLoading}>
116
+ {#if register.isLoading}
128
117
  <span class="loading-spinner"></span>
129
118
  {/if}
130
119
  {t('auth.registerButton')}
@@ -190,7 +179,7 @@
190
179
  position: relative;
191
180
  }
192
181
 
193
- .input-icon {
182
+ :global(.input-icon) {
194
183
  position: absolute;
195
184
  left: 0.75rem;
196
185
  top: 50%;
@@ -43,15 +43,15 @@
43
43
  {/if}
44
44
  </div>
45
45
 
46
- {#if query.isLoading}
46
+ {#if query.query.isLoading}
47
47
  <div class="flex h-64 items-center justify-center">
48
48
  <Loader2 class="h-6 w-6 animate-spin text-primary" />
49
49
  </div>
50
- {:else if query.data}
50
+ {:else if query.query.data?.data}
51
51
  <Card.Root>
52
52
  <Card.Content class="divide-y divide-border p-0">
53
53
  {#each showFields as field}
54
- {@const value = (query.data as Record<string, unknown>)[field.key]}
54
+ {@const value = (query.query.data!.data as Record<string, unknown>)[field.key]}
55
55
  <div class="flex px-6 py-4">
56
56
  <div class="w-1/3 text-sm font-medium text-muted-foreground">{field.label}</div>
57
57
  <div class="w-2/3 text-sm text-foreground">