@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.
@@ -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>
@@ -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%;
@@ -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%;
@@ -1,12 +1,31 @@
1
1
  <script lang="ts">
2
2
  import { Loader2 } from 'lucide-svelte';
3
3
 
4
+ type ColorVariant = 'primary' | 'success' | 'warning' | 'danger' | 'info';
5
+ type StyleVariant = 'default' | 'outline' | 'filled';
6
+
7
+ const colorMap: Record<ColorVariant, string> = {
8
+ primary: 'bg-primary/10 text-primary',
9
+ success: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
10
+ warning: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
11
+ danger: 'bg-red-500/10 text-red-600 dark:text-red-400',
12
+ info: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
13
+ };
14
+
15
+ const variantMap: Record<StyleVariant, string> = {
16
+ default: 'border border-border bg-card shadow-sm',
17
+ outline: 'border-2 border-border bg-transparent',
18
+ filled: 'border-0 bg-muted/50 shadow-sm',
19
+ };
20
+
4
21
  interface Props {
5
22
  label: string;
6
23
  value: string | number;
7
24
  icon?: typeof Loader2;
8
25
  trend?: { value: number; label?: string };
9
26
  loading?: boolean;
27
+ color?: ColorVariant;
28
+ variant?: StyleVariant;
10
29
  class?: string;
11
30
  }
12
31
 
@@ -16,13 +35,15 @@
16
35
  icon: Icon,
17
36
  trend,
18
37
  loading = false,
38
+ color = 'primary',
39
+ variant = 'default',
19
40
  class: className = '',
20
41
  }: Props = $props();
21
42
  </script>
22
43
 
23
- <div class="flex items-center gap-4 rounded-xl border border-border bg-card p-5 shadow-sm {className}">
44
+ <div class="flex items-center gap-4 rounded-xl p-5 {variantMap[variant]} {className}">
24
45
  {#if Icon}
25
- <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
46
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl {colorMap[color]}">
26
47
  <Icon class="h-6 w-6" />
27
48
  </div>
28
49
  {/if}
@@ -0,0 +1,217 @@
1
+ <script lang="ts">
2
+ import { useUpdatePassword } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import { navigate } from '@svadmin/core/router';
5
+ import { Button } from './ui/button/index.js';
6
+ import { Input } from './ui/input/index.js';
7
+ import * as Card from './ui/card/index.js';
8
+ import { Lock, Eye, EyeOff, ShieldCheck } from 'lucide-svelte';
9
+
10
+ let { title = 'Admin' } = $props<{
11
+ title?: string;
12
+ }>();
13
+
14
+ const updatePw = useUpdatePassword();
15
+
16
+ let password = $state('');
17
+ let confirmPassword = $state('');
18
+ let showPassword = $state(false);
19
+ let error = $state('');
20
+
21
+ async function handleSubmit(e: SubmitEvent) {
22
+ e.preventDefault();
23
+ error = '';
24
+
25
+ if (!password) { error = t('auth.passwordRequired'); return; }
26
+ if (password !== confirmPassword) { error = t('auth.passwordMismatch'); return; }
27
+
28
+ const result = await updatePw.mutate({ password, confirmPassword });
29
+ if (!result.success) {
30
+ error = result.error?.message ?? t('common.operationFailed');
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <div class="auth-page">
36
+ <div class="auth-container">
37
+ <Card.Card class="auth-card">
38
+ <Card.CardHeader class="auth-header">
39
+ <div class="auth-icon">
40
+ <ShieldCheck class="h-6 w-6" />
41
+ </div>
42
+ <Card.CardTitle class="text-2xl font-bold">{t('auth.resetPassword')}</Card.CardTitle>
43
+ <p class="text-sm text-muted-foreground">Enter your new password below.</p>
44
+ </Card.CardHeader>
45
+ <Card.CardContent>
46
+ <form onsubmit={handleSubmit} class="space-y-4">
47
+ {#if error}
48
+ <div class="error-alert">
49
+ <p>{error}</p>
50
+ </div>
51
+ {/if}
52
+
53
+ <div class="space-y-2">
54
+ <label for="new-password" class="text-sm font-medium text-foreground">{t('auth.password')}</label>
55
+ <div class="input-with-icon">
56
+ <Lock class="input-icon h-4 w-4" />
57
+ <Input
58
+ id="new-password"
59
+ type={showPassword ? 'text' : 'password'}
60
+ placeholder="••••••••"
61
+ bind:value={password}
62
+ class="pl-9 pr-9"
63
+ autocomplete="new-password"
64
+ />
65
+ <button
66
+ type="button"
67
+ class="password-toggle"
68
+ onclick={() => showPassword = !showPassword}
69
+ tabindex={-1}
70
+ >
71
+ {#if showPassword}
72
+ <EyeOff class="h-4 w-4" />
73
+ {:else}
74
+ <Eye class="h-4 w-4" />
75
+ {/if}
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="space-y-2">
81
+ <label for="confirm-password" class="text-sm font-medium text-foreground">{t('auth.confirmPassword')}</label>
82
+ <div class="input-with-icon">
83
+ <Lock class="input-icon h-4 w-4" />
84
+ <Input
85
+ id="confirm-password"
86
+ type={showPassword ? 'text' : 'password'}
87
+ placeholder="••••••••"
88
+ bind:value={confirmPassword}
89
+ class="pl-9"
90
+ autocomplete="new-password"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <Button type="submit" class="w-full" disabled={updatePw.isLoading}>
96
+ {#if updatePw.isLoading}
97
+ <span class="loading-spinner"></span>
98
+ {/if}
99
+ {t('auth.resetPassword')}
100
+ </Button>
101
+
102
+ <div class="auth-link">
103
+ <button
104
+ type="button"
105
+ class="text-sm text-primary hover:underline font-medium"
106
+ onclick={() => navigate('/login')}
107
+ >{t('auth.backToLogin')}</button>
108
+ </div>
109
+ </form>
110
+ </Card.CardContent>
111
+ </Card.Card>
112
+
113
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
114
+ Powered by {title}
115
+ </p>
116
+ </div>
117
+ </div>
118
+
119
+ <style>
120
+ .auth-page {
121
+ min-height: 100vh;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
126
+ padding: 1rem;
127
+ }
128
+
129
+ .auth-container {
130
+ width: 100%;
131
+ max-width: 420px;
132
+ }
133
+
134
+ :global(.auth-card) {
135
+ backdrop-filter: blur(20px);
136
+ border: 1px solid hsl(var(--border) / 0.5);
137
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
138
+ }
139
+
140
+ :global(.auth-header) {
141
+ text-align: center;
142
+ padding-bottom: 0.5rem;
143
+ }
144
+
145
+ .auth-icon {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: 48px;
150
+ height: 48px;
151
+ border-radius: 12px;
152
+ background: hsl(var(--primary) / 0.1);
153
+ color: hsl(var(--primary));
154
+ margin: 0 auto 0.75rem;
155
+ }
156
+
157
+ .input-with-icon {
158
+ position: relative;
159
+ }
160
+
161
+ :global(.input-icon) {
162
+ position: absolute;
163
+ left: 0.75rem;
164
+ top: 50%;
165
+ transform: translateY(-50%);
166
+ color: hsl(var(--muted-foreground));
167
+ pointer-events: none;
168
+ z-index: 1;
169
+ }
170
+
171
+ .password-toggle {
172
+ position: absolute;
173
+ right: 0.75rem;
174
+ top: 50%;
175
+ transform: translateY(-50%);
176
+ color: hsl(var(--muted-foreground));
177
+ background: none;
178
+ border: none;
179
+ cursor: pointer;
180
+ padding: 2px;
181
+ z-index: 1;
182
+ }
183
+ .password-toggle:hover {
184
+ color: hsl(var(--foreground));
185
+ }
186
+
187
+ .error-alert {
188
+ padding: 0.75rem;
189
+ border-radius: 0.5rem;
190
+ background: hsl(var(--destructive) / 0.1);
191
+ border: 1px solid hsl(var(--destructive) / 0.3);
192
+ color: hsl(var(--destructive));
193
+ font-size: 0.875rem;
194
+ }
195
+
196
+ .loading-spinner {
197
+ display: inline-block;
198
+ width: 16px;
199
+ height: 16px;
200
+ border: 2px solid transparent;
201
+ border-top-color: currentColor;
202
+ border-radius: 50%;
203
+ animation: spin 0.6s linear infinite;
204
+ margin-right: 0.5rem;
205
+ }
206
+
207
+ @keyframes spin {
208
+ to { transform: rotate(360deg); }
209
+ }
210
+
211
+ .auth-link {
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ margin-top: 0.5rem;
216
+ }
217
+ </style>
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { useNavigation, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Copy } from 'lucide-svelte';
5
+
6
+ let { resource, recordItemId, hideText = false, accessControl, class: className = '' } = $props<{
7
+ resource: string;
8
+ recordItemId: string | number;
9
+ hideText?: boolean;
10
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
11
+ class?: string;
12
+ }>();
13
+
14
+ const nav = useNavigation();
15
+ const can = accessControl?.enabled ? useCan({ resource, action: 'create' }) : null;
16
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
17
+ </script>
18
+
19
+ {#if !hidden}
20
+ <Button
21
+ variant="outline"
22
+ size={hideText ? 'icon' : 'sm'}
23
+ class={className}
24
+ disabled={can ? !can.allowed : false}
25
+ onclick={() => nav.clone(resource, recordItemId)}
26
+ >
27
+ <Copy class="h-4 w-4" />
28
+ {#if !hideText}<span class="ml-1">{t('common.clone')}</span>{/if}
29
+ </Button>
30
+ {/if}
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { useNavigation, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Plus } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, accessControl, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
10
+ class?: string;
11
+ }>();
12
+
13
+ const nav = useNavigation();
14
+ const can = accessControl?.enabled ? useCan({ resource, action: 'create' }) : null;
15
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
16
+ </script>
17
+
18
+ {#if !hidden}
19
+ <Button
20
+ variant="default"
21
+ size={hideText ? 'icon' : 'default'}
22
+ class={className}
23
+ disabled={can ? !can.allowed : false}
24
+ onclick={() => nav.create(resource)}
25
+ >
26
+ <Plus class="h-4 w-4" />
27
+ {#if !hideText}<span class="ml-1">{t('common.create')}</span>{/if}
28
+ </Button>
29
+ {/if}