@svadmin/ui 0.0.2 → 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.
Files changed (29) hide show
  1. package/package.json +2 -2
  2. package/src/components/AdminApp.svelte +35 -15
  3. package/src/components/Authenticated.svelte +26 -0
  4. package/src/components/AutoForm.svelte +57 -5
  5. package/src/components/CanAccess.svelte +21 -0
  6. package/src/components/ConfigErrorScreen.svelte +224 -0
  7. package/src/components/DevTools.svelte +280 -0
  8. package/src/components/DrawerForm.svelte +26 -0
  9. package/src/components/ForgotPasswordPage.svelte +203 -0
  10. package/src/components/InferencerPanel.svelte +166 -0
  11. package/src/components/LoginPage.svelte +236 -0
  12. package/src/components/ModalForm.svelte +34 -0
  13. package/src/components/RegisterPage.svelte +241 -0
  14. package/src/components/StatsCard.svelte +23 -2
  15. package/src/components/UndoableNotification.svelte +132 -0
  16. package/src/components/UpdatePasswordPage.svelte +217 -0
  17. package/src/components/buttons/CloneButton.svelte +30 -0
  18. package/src/components/buttons/CreateButton.svelte +29 -0
  19. package/src/components/buttons/DeleteButton.svelte +60 -0
  20. package/src/components/buttons/EditButton.svelte +30 -0
  21. package/src/components/buttons/ExportButton.svelte +24 -0
  22. package/src/components/buttons/ImportButton.svelte +28 -0
  23. package/src/components/buttons/ListButton.svelte +23 -0
  24. package/src/components/buttons/RefreshButton.svelte +30 -0
  25. package/src/components/buttons/SaveButton.svelte +27 -0
  26. package/src/components/buttons/ShowButton.svelte +24 -0
  27. package/src/components/buttons/index.ts +10 -0
  28. package/src/index.ts +18 -0
  29. package/src/router-state.svelte.ts +26 -5
@@ -0,0 +1,236 @@
1
+ <script lang="ts">
2
+ import { useLogin, getAuthProvider } 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 { LogIn, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
9
+
10
+ let { title = 'Admin', onSuccess } = $props<{
11
+ title?: string;
12
+ onSuccess?: () => void;
13
+ }>();
14
+
15
+ const login = useLogin();
16
+ const authProvider = getAuthProvider();
17
+
18
+ let email = $state('');
19
+ let password = $state('');
20
+ let showPassword = $state(false);
21
+ let error = $state('');
22
+
23
+ async function handleSubmit(e: SubmitEvent) {
24
+ e.preventDefault();
25
+ error = '';
26
+
27
+ if (!email) { error = t('auth.emailRequired'); return; }
28
+ if (!password) { error = t('auth.passwordRequired'); return; }
29
+
30
+ const result = await login.mutate({ email, password });
31
+ if (result.success) {
32
+ onSuccess?.();
33
+ } else {
34
+ error = result.error?.message ?? t('common.loginFailed');
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <div class="login-page">
40
+ <div class="login-container">
41
+ <Card.Card class="login-card">
42
+ <Card.CardHeader class="login-header">
43
+ <div class="login-icon">
44
+ <LogIn class="h-6 w-6" />
45
+ </div>
46
+ <Card.CardTitle class="text-2xl font-bold">{t('auth.welcomeBack')}</Card.CardTitle>
47
+ <p class="text-sm text-muted-foreground">{t('auth.welcomeMessage')}</p>
48
+ </Card.CardHeader>
49
+ <Card.CardContent>
50
+ <form onsubmit={handleSubmit} class="space-y-4">
51
+ {#if error}
52
+ <div class="error-alert">
53
+ <p>{error}</p>
54
+ </div>
55
+ {/if}
56
+
57
+ <div class="space-y-2">
58
+ <label for="login-email" class="text-sm font-medium text-foreground">{t('auth.email')}</label>
59
+ <div class="input-with-icon">
60
+ <Mail class="input-icon h-4 w-4" />
61
+ <Input
62
+ id="login-email"
63
+ type="email"
64
+ placeholder="name@example.com"
65
+ bind:value={email}
66
+ class="pl-9"
67
+ autocomplete="email"
68
+ />
69
+ </div>
70
+ </div>
71
+
72
+ <div class="space-y-2">
73
+ <div class="flex items-center justify-between">
74
+ <label for="login-password" class="text-sm font-medium text-foreground">{t('auth.password')}</label>
75
+ {#if authProvider.forgotPassword}
76
+ <button
77
+ type="button"
78
+ class="text-xs text-primary hover:underline"
79
+ onclick={() => navigate('/forgot-password')}
80
+ >{t('auth.forgotPasswordLink')}</button>
81
+ {/if}
82
+ </div>
83
+ <div class="input-with-icon">
84
+ <Lock class="input-icon h-4 w-4" />
85
+ <Input
86
+ id="login-password"
87
+ type={showPassword ? 'text' : 'password'}
88
+ placeholder="••••••••"
89
+ bind:value={password}
90
+ class="pl-9 pr-9"
91
+ autocomplete="current-password"
92
+ />
93
+ <button
94
+ type="button"
95
+ class="password-toggle"
96
+ onclick={() => showPassword = !showPassword}
97
+ tabindex={-1}
98
+ >
99
+ {#if showPassword}
100
+ <EyeOff class="h-4 w-4" />
101
+ {:else}
102
+ <Eye class="h-4 w-4" />
103
+ {/if}
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <Button type="submit" class="w-full" disabled={login.isLoading}>
109
+ {#if login.isLoading}
110
+ <span class="loading-spinner"></span>
111
+ {/if}
112
+ {t('auth.loginButton')}
113
+ </Button>
114
+ </form>
115
+
116
+ {#if authProvider.register}
117
+ <div class="auth-link">
118
+ <span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
119
+ <button
120
+ type="button"
121
+ class="text-sm text-primary hover:underline font-medium"
122
+ onclick={() => navigate('/register')}
123
+ >{t('auth.register')}</button>
124
+ </div>
125
+ {/if}
126
+ </Card.CardContent>
127
+ </Card.Card>
128
+
129
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
130
+ Powered by {title}
131
+ </p>
132
+ </div>
133
+ </div>
134
+
135
+ <style>
136
+ .login-page {
137
+ min-height: 100vh;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
142
+ padding: 1rem;
143
+ }
144
+
145
+ .login-container {
146
+ width: 100%;
147
+ max-width: 420px;
148
+ }
149
+
150
+ :global(.login-card) {
151
+ backdrop-filter: blur(20px);
152
+ border: 1px solid hsl(var(--border) / 0.5);
153
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
154
+ }
155
+
156
+ :global(.login-header) {
157
+ text-align: center;
158
+ padding-bottom: 0.5rem;
159
+ }
160
+
161
+ .login-icon {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ width: 48px;
166
+ height: 48px;
167
+ border-radius: 12px;
168
+ background: hsl(var(--primary) / 0.1);
169
+ color: hsl(var(--primary));
170
+ margin: 0 auto 0.75rem;
171
+ }
172
+
173
+ .input-with-icon {
174
+ position: relative;
175
+ }
176
+
177
+ :global(.input-icon) {
178
+ position: absolute;
179
+ left: 0.75rem;
180
+ top: 50%;
181
+ transform: translateY(-50%);
182
+ color: hsl(var(--muted-foreground));
183
+ pointer-events: none;
184
+ z-index: 1;
185
+ }
186
+
187
+ .password-toggle {
188
+ position: absolute;
189
+ right: 0.75rem;
190
+ top: 50%;
191
+ transform: translateY(-50%);
192
+ color: hsl(var(--muted-foreground));
193
+ background: none;
194
+ border: none;
195
+ cursor: pointer;
196
+ padding: 2px;
197
+ z-index: 1;
198
+ }
199
+ .password-toggle:hover {
200
+ color: hsl(var(--foreground));
201
+ }
202
+
203
+ .error-alert {
204
+ padding: 0.75rem;
205
+ border-radius: 0.5rem;
206
+ background: hsl(var(--destructive) / 0.1);
207
+ border: 1px solid hsl(var(--destructive) / 0.3);
208
+ color: hsl(var(--destructive));
209
+ font-size: 0.875rem;
210
+ }
211
+
212
+ .loading-spinner {
213
+ display: inline-block;
214
+ width: 16px;
215
+ height: 16px;
216
+ border: 2px solid transparent;
217
+ border-top-color: currentColor;
218
+ border-radius: 50%;
219
+ animation: spin 0.6s linear infinite;
220
+ margin-right: 0.5rem;
221
+ }
222
+
223
+ @keyframes spin {
224
+ to { transform: rotate(360deg); }
225
+ }
226
+
227
+ .auth-link {
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ gap: 0.25rem;
232
+ margin-top: 1.25rem;
233
+ padding-top: 1.25rem;
234
+ border-top: 1px solid hsl(var(--border));
235
+ }
236
+ </style>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { FieldDefinition } from '@svadmin/core';
4
+ import { getResource, useForm } from '@svadmin/core';
5
+ import * as Dialog from './ui/dialog/index.js';
6
+ import AutoForm from './AutoForm.svelte';
7
+
8
+ let { resourceName, mode = 'create', id, open = $bindable(false), onSuccess } = $props<{
9
+ resourceName: string;
10
+ mode?: 'create' | 'edit';
11
+ id?: string | number;
12
+ open: boolean;
13
+ onSuccess?: () => void;
14
+ }>();
15
+
16
+ const resource = $derived(getResource(resourceName));
17
+
18
+ function handleClose() {
19
+ open = false;
20
+ }
21
+ </script>
22
+
23
+ {#if open}
24
+ <Dialog.Dialog bind:open>
25
+ <Dialog.DialogContent class="sm:max-w-2xl max-h-[85vh] overflow-y-auto">
26
+ <Dialog.DialogHeader>
27
+ <Dialog.DialogTitle>
28
+ {mode === 'create' ? `Create ${resource.label}` : `Edit ${resource.label}`}
29
+ </Dialog.DialogTitle>
30
+ </Dialog.DialogHeader>
31
+ <AutoForm {resourceName} {mode} {id} />
32
+ </Dialog.DialogContent>
33
+ </Dialog.Dialog>
34
+ {/if}
@@ -0,0 +1,241 @@
1
+ <script lang="ts">
2
+ import { useRegister } 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 { UserPlus, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
9
+
10
+ let { title = 'Admin', onSuccess } = $props<{
11
+ title?: string;
12
+ onSuccess?: () => void;
13
+ }>();
14
+
15
+ const register = useRegister();
16
+
17
+ let email = $state('');
18
+ let password = $state('');
19
+ let confirmPassword = $state('');
20
+ let showPassword = $state(false);
21
+ let error = $state('');
22
+
23
+ async function handleSubmit(e: SubmitEvent) {
24
+ e.preventDefault();
25
+ error = '';
26
+
27
+ if (!email) { error = t('auth.emailRequired'); return; }
28
+ if (!password) { error = t('auth.passwordRequired'); return; }
29
+ if (password !== confirmPassword) { error = t('auth.passwordMismatch'); return; }
30
+
31
+ const result = await register.mutate({ email, password });
32
+ if (result.success) {
33
+ onSuccess?.();
34
+ } else {
35
+ error = result.error?.message ?? t('common.operationFailed');
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <div class="register-page">
41
+ <div class="register-container">
42
+ <Card.Card class="login-card">
43
+ <Card.CardHeader class="login-header">
44
+ <div class="register-icon">
45
+ <UserPlus class="h-6 w-6" />
46
+ </div>
47
+ <Card.CardTitle class="text-2xl font-bold">{t('auth.createAccount')}</Card.CardTitle>
48
+ <p class="text-sm text-muted-foreground">{t('auth.createAccountMessage')}</p>
49
+ </Card.CardHeader>
50
+ <Card.CardContent>
51
+ <form onsubmit={handleSubmit} class="space-y-4">
52
+ {#if error}
53
+ <div class="error-alert">
54
+ <p>{error}</p>
55
+ </div>
56
+ {/if}
57
+
58
+ <div class="space-y-2">
59
+ <label for="register-email" class="text-sm font-medium text-foreground">{t('auth.email')}</label>
60
+ <div class="input-with-icon">
61
+ <Mail class="input-icon h-4 w-4" />
62
+ <Input
63
+ id="register-email"
64
+ type="email"
65
+ placeholder="name@example.com"
66
+ bind:value={email}
67
+ class="pl-9"
68
+ autocomplete="email"
69
+ />
70
+ </div>
71
+ </div>
72
+
73
+ <div class="space-y-2">
74
+ <label for="register-password" class="text-sm font-medium text-foreground">{t('auth.password')}</label>
75
+ <div class="input-with-icon">
76
+ <Lock class="input-icon h-4 w-4" />
77
+ <Input
78
+ id="register-password"
79
+ type={showPassword ? 'text' : 'password'}
80
+ placeholder="••••••••"
81
+ bind:value={password}
82
+ class="pl-9 pr-9"
83
+ autocomplete="new-password"
84
+ />
85
+ <button
86
+ type="button"
87
+ class="password-toggle"
88
+ onclick={() => showPassword = !showPassword}
89
+ tabindex={-1}
90
+ >
91
+ {#if showPassword}
92
+ <EyeOff class="h-4 w-4" />
93
+ {:else}
94
+ <Eye class="h-4 w-4" />
95
+ {/if}
96
+ </button>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="space-y-2">
101
+ <label for="register-confirm" class="text-sm font-medium text-foreground">{t('auth.confirmPassword')}</label>
102
+ <div class="input-with-icon">
103
+ <Lock class="input-icon h-4 w-4" />
104
+ <Input
105
+ id="register-confirm"
106
+ type={showPassword ? 'text' : 'password'}
107
+ placeholder="••••••••"
108
+ bind:value={confirmPassword}
109
+ class="pl-9"
110
+ autocomplete="new-password"
111
+ />
112
+ </div>
113
+ </div>
114
+
115
+ <Button type="submit" class="w-full" disabled={register.isLoading}>
116
+ {#if register.isLoading}
117
+ <span class="loading-spinner"></span>
118
+ {/if}
119
+ {t('auth.registerButton')}
120
+ </Button>
121
+ </form>
122
+
123
+ <div class="auth-link">
124
+ <span class="text-sm text-muted-foreground">{t('auth.hasAccount')}</span>
125
+ <button
126
+ type="button"
127
+ class="text-sm text-primary hover:underline font-medium"
128
+ onclick={() => navigate('/login')}
129
+ >{t('auth.login')}</button>
130
+ </div>
131
+ </Card.CardContent>
132
+ </Card.Card>
133
+
134
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
135
+ Powered by {title}
136
+ </p>
137
+ </div>
138
+ </div>
139
+
140
+ <style>
141
+ .register-page {
142
+ min-height: 100vh;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
147
+ padding: 1rem;
148
+ }
149
+
150
+ .register-container {
151
+ width: 100%;
152
+ max-width: 420px;
153
+ }
154
+
155
+ .register-icon {
156
+ display: inline-flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ width: 48px;
160
+ height: 48px;
161
+ border-radius: 12px;
162
+ background: hsl(var(--primary) / 0.1);
163
+ color: hsl(var(--primary));
164
+ margin: 0 auto 0.75rem;
165
+ }
166
+
167
+ :global(.login-card) {
168
+ backdrop-filter: blur(20px);
169
+ border: 1px solid hsl(var(--border) / 0.5);
170
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
171
+ }
172
+
173
+ :global(.login-header) {
174
+ text-align: center;
175
+ padding-bottom: 0.5rem;
176
+ }
177
+
178
+ .input-with-icon {
179
+ position: relative;
180
+ }
181
+
182
+ :global(.input-icon) {
183
+ position: absolute;
184
+ left: 0.75rem;
185
+ top: 50%;
186
+ transform: translateY(-50%);
187
+ color: hsl(var(--muted-foreground));
188
+ pointer-events: none;
189
+ z-index: 1;
190
+ }
191
+
192
+ .password-toggle {
193
+ position: absolute;
194
+ right: 0.75rem;
195
+ top: 50%;
196
+ transform: translateY(-50%);
197
+ color: hsl(var(--muted-foreground));
198
+ background: none;
199
+ border: none;
200
+ cursor: pointer;
201
+ padding: 2px;
202
+ z-index: 1;
203
+ }
204
+ .password-toggle:hover {
205
+ color: hsl(var(--foreground));
206
+ }
207
+
208
+ .error-alert {
209
+ padding: 0.75rem;
210
+ border-radius: 0.5rem;
211
+ background: hsl(var(--destructive) / 0.1);
212
+ border: 1px solid hsl(var(--destructive) / 0.3);
213
+ color: hsl(var(--destructive));
214
+ font-size: 0.875rem;
215
+ }
216
+
217
+ .loading-spinner {
218
+ display: inline-block;
219
+ width: 16px;
220
+ height: 16px;
221
+ border: 2px solid transparent;
222
+ border-top-color: currentColor;
223
+ border-radius: 50%;
224
+ animation: spin 0.6s linear infinite;
225
+ margin-right: 0.5rem;
226
+ }
227
+
228
+ @keyframes spin {
229
+ to { transform: rotate(360deg); }
230
+ }
231
+
232
+ .auth-link {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ gap: 0.25rem;
237
+ margin-top: 1.25rem;
238
+ padding-top: 1.25rem;
239
+ border-top: 1px solid hsl(var(--border));
240
+ }
241
+ </style>
@@ -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,132 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { X, Undo2 } from 'lucide-svelte';
4
+
5
+ let { message, duration = 5000, onUndo, onTimeout } = $props<{
6
+ message: string;
7
+ duration?: number;
8
+ onUndo: () => void;
9
+ onTimeout: () => void;
10
+ }>();
11
+
12
+ let remaining = $state(duration);
13
+ let dismissed = $state(false);
14
+
15
+ const interval = setInterval(() => {
16
+ remaining -= 100;
17
+ if (remaining <= 0) {
18
+ clearInterval(interval);
19
+ if (!dismissed) {
20
+ dismissed = true;
21
+ onTimeout();
22
+ }
23
+ }
24
+ }, 100);
25
+
26
+ function handleUndo() {
27
+ clearInterval(interval);
28
+ dismissed = true;
29
+ onUndo();
30
+ }
31
+
32
+ function handleDismiss() {
33
+ clearInterval(interval);
34
+ dismissed = true;
35
+ onTimeout();
36
+ }
37
+
38
+ const progress = $derived(remaining / duration * 100);
39
+ </script>
40
+
41
+ {#if !dismissed}
42
+ <div class="undoable-notification">
43
+ <div class="undoable-content">
44
+ <p class="undoable-message">{message}</p>
45
+ <div class="undoable-actions">
46
+ <Button variant="ghost" size="sm" onclick={handleUndo} class="undoable-undo-btn">
47
+ <Undo2 class="h-3.5 w-3.5 mr-1" />
48
+ Undo
49
+ </Button>
50
+ <button class="undoable-close" onclick={handleDismiss}>
51
+ <X class="h-3.5 w-3.5" />
52
+ </button>
53
+ </div>
54
+ </div>
55
+ <div class="undoable-progress">
56
+ <div class="undoable-progress-bar" style="width: {progress}%"></div>
57
+ </div>
58
+ </div>
59
+ {/if}
60
+
61
+ <style>
62
+ .undoable-notification {
63
+ position: fixed;
64
+ bottom: 1.5rem;
65
+ left: 50%;
66
+ transform: translateX(-50%);
67
+ z-index: 100;
68
+ min-width: 320px;
69
+ max-width: 480px;
70
+ background: hsl(var(--card));
71
+ border: 1px solid hsl(var(--border));
72
+ border-radius: 0.75rem;
73
+ box-shadow: 0 8px 32px hsl(0 0% 0% / 0.15);
74
+ overflow: hidden;
75
+ animation: slideUp 0.3s ease;
76
+ }
77
+
78
+ .undoable-content {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ padding: 0.75rem 1rem;
83
+ gap: 0.75rem;
84
+ }
85
+
86
+ .undoable-message {
87
+ font-size: 0.875rem;
88
+ color: hsl(var(--foreground));
89
+ margin: 0;
90
+ }
91
+
92
+ .undoable-actions {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 0.25rem;
96
+ flex-shrink: 0;
97
+ }
98
+
99
+ :global(.undoable-undo-btn) {
100
+ font-weight: 600;
101
+ color: hsl(var(--primary)) !important;
102
+ }
103
+
104
+ .undoable-close {
105
+ background: none;
106
+ border: none;
107
+ cursor: pointer;
108
+ padding: 0.25rem;
109
+ color: hsl(var(--muted-foreground));
110
+ border-radius: 0.25rem;
111
+ }
112
+ .undoable-close:hover {
113
+ color: hsl(var(--foreground));
114
+ background: hsl(var(--muted));
115
+ }
116
+
117
+ .undoable-progress {
118
+ height: 3px;
119
+ background: hsl(var(--muted));
120
+ }
121
+
122
+ .undoable-progress-bar {
123
+ height: 100%;
124
+ background: hsl(var(--primary));
125
+ transition: width 100ms linear;
126
+ }
127
+
128
+ @keyframes slideUp {
129
+ from { transform: translateX(-50%) translateY(100%); opacity: 0; }
130
+ to { transform: translateX(-50%) translateY(0); opacity: 1; }
131
+ }
132
+ </style>