@svadmin/ui 0.0.6 → 0.4.1

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 (120) hide show
  1. package/package.json +8 -3
  2. package/src/actions.ts +110 -0
  3. package/src/components/AdminApp.svelte +8 -4
  4. package/src/components/Authenticated.svelte +38 -13
  5. package/src/components/AutoForm.svelte +163 -74
  6. package/src/components/AutoSaveIndicator.svelte +35 -0
  7. package/src/components/AutoTable.svelte +476 -183
  8. package/src/components/Breadcrumbs.svelte +22 -17
  9. package/src/components/CatchAllNavigate.svelte +21 -0
  10. package/src/components/ComboboxField.svelte +122 -0
  11. package/src/components/CommandPalette.svelte +93 -0
  12. package/src/components/ConfigErrorScreen.svelte +53 -151
  13. package/src/components/ConfirmDialog.svelte +25 -17
  14. package/src/components/CreatePage.svelte +43 -0
  15. package/src/components/DevTools.svelte +99 -230
  16. package/src/components/DrawerForm.svelte +19 -10
  17. package/src/components/EditPage.svelte +71 -0
  18. package/src/components/EmptyState.svelte +21 -31
  19. package/src/components/ErrorBoundary.svelte +38 -40
  20. package/src/components/ErrorComponent.svelte +36 -0
  21. package/src/components/FieldRenderer.svelte +240 -75
  22. package/src/components/ForgotPasswordPage.svelte +20 -111
  23. package/src/components/Header.svelte +46 -57
  24. package/src/components/InferencerPanel.svelte +136 -110
  25. package/src/components/InfiniteList.svelte +93 -0
  26. package/src/components/Layout.svelte +85 -22
  27. package/src/components/ListPage.svelte +71 -0
  28. package/src/components/LiveIndicator.svelte +58 -0
  29. package/src/components/LoginPage.svelte +33 -154
  30. package/src/components/ModalForm.svelte +6 -2
  31. package/src/components/NavigateToResource.svelte +20 -0
  32. package/src/components/PageSkeleton.svelte +71 -0
  33. package/src/components/PasswordInput.svelte +95 -0
  34. package/src/components/RegisterPage.svelte +35 -164
  35. package/src/components/ShowPage.svelte +35 -36
  36. package/src/components/Sidebar.svelte +212 -81
  37. package/src/components/StatsCard.svelte +12 -3
  38. package/src/components/StepsForm.svelte +139 -0
  39. package/src/components/ThemedTitle.svelte +32 -0
  40. package/src/components/Toast.svelte +15 -44
  41. package/src/components/TooltipButton.svelte +52 -0
  42. package/src/components/UndoableNotification.svelte +28 -96
  43. package/src/components/UpdatePasswordPage.svelte +32 -160
  44. package/src/components/buttons/CloneButton.svelte +1 -1
  45. package/src/components/buttons/CreateButton.svelte +1 -1
  46. package/src/components/buttons/DeleteButton.svelte +3 -4
  47. package/src/components/buttons/EditButton.svelte +1 -1
  48. package/src/components/buttons/ImportButton.svelte +29 -7
  49. package/src/components/buttons/RefreshButton.svelte +11 -5
  50. package/src/components/fieldComponentMap.ts +79 -0
  51. package/src/components/fields/BooleanField.svelte +13 -0
  52. package/src/components/fields/DateField.svelte +6 -0
  53. package/src/components/fields/EmailField.svelte +9 -0
  54. package/src/components/fields/FileField.svelte +14 -0
  55. package/src/components/fields/ImageField.svelte +9 -0
  56. package/src/components/fields/JsonField.svelte +72 -0
  57. package/src/components/fields/MarkdownField.svelte +9 -0
  58. package/src/components/fields/MultiSelectField.svelte +20 -0
  59. package/src/components/fields/NumberField.svelte +6 -0
  60. package/src/components/fields/RelationField.svelte +24 -0
  61. package/src/components/fields/RichTextField.svelte +35 -0
  62. package/src/components/fields/SelectField.svelte +14 -0
  63. package/src/components/fields/TagField.svelte +12 -0
  64. package/src/components/fields/TextField.svelte +11 -0
  65. package/src/components/fields/UrlField.svelte +14 -0
  66. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
  67. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
  68. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
  69. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
  70. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
  71. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
  72. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
  73. package/src/components/ui/alert-dialog/index.ts +20 -0
  74. package/src/components/ui/avatar/avatar.svelte +2 -1
  75. package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
  76. package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
  77. package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
  78. package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
  79. package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
  80. package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
  81. package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
  82. package/src/components/ui/breadcrumb/index.ts +7 -0
  83. package/src/components/ui/collapsible/index.ts +7 -0
  84. package/src/components/ui/command/command-dialog.svelte +31 -0
  85. package/src/components/ui/command/index.ts +2 -0
  86. package/src/components/ui/context-menu/index.ts +11 -0
  87. package/src/components/ui/drawer/drawer-content.svelte +21 -0
  88. package/src/components/ui/drawer/drawer-description.svelte +11 -0
  89. package/src/components/ui/drawer/drawer-footer.svelte +11 -0
  90. package/src/components/ui/drawer/drawer-header.svelte +11 -0
  91. package/src/components/ui/drawer/drawer-title.svelte +11 -0
  92. package/src/components/ui/drawer/drawer.svelte +18 -0
  93. package/src/components/ui/drawer/index.ts +6 -0
  94. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
  95. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
  96. package/src/components/ui/dropdown-menu/index.ts +6 -0
  97. package/src/components/ui/label/index.ts +1 -0
  98. package/src/components/ui/label/label.svelte +18 -0
  99. package/src/components/ui/pagination/index.ts +7 -0
  100. package/src/components/ui/pagination/pagination-content.svelte +11 -0
  101. package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
  102. package/src/components/ui/pagination/pagination-item.svelte +11 -0
  103. package/src/components/ui/pagination/pagination-link.svelte +26 -0
  104. package/src/components/ui/pagination/pagination-next.svelte +22 -0
  105. package/src/components/ui/pagination/pagination-previous.svelte +22 -0
  106. package/src/components/ui/pagination/pagination.svelte +11 -0
  107. package/src/components/ui/popover/index.ts +12 -0
  108. package/src/components/ui/popover/popover-content.svelte +25 -0
  109. package/src/components/ui/progress/index.ts +1 -0
  110. package/src/components/ui/progress/progress.svelte +22 -0
  111. package/src/components/ui/scroll-area/index.ts +1 -0
  112. package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
  113. package/src/components/ui/sheet/index.ts +15 -1
  114. package/src/components/ui/sheet/sheet-content.svelte +28 -0
  115. package/src/components/ui/sheet/sheet-description.svelte +22 -0
  116. package/src/components/ui/sheet/sheet-footer.svelte +22 -0
  117. package/src/components/ui/sheet/sheet-header.svelte +22 -0
  118. package/src/components/ui/sheet/sheet-title.svelte +22 -0
  119. package/src/index.ts +52 -1
  120. package/src/router-state.svelte.ts +16 -9
@@ -4,8 +4,11 @@
4
4
  import { navigate } from '@svadmin/core/router';
5
5
  import { Button } from './ui/button/index.js';
6
6
  import { Input } from './ui/input/index.js';
7
+ import { Label } from './ui/label/index.js';
7
8
  import * as Card from './ui/card/index.js';
8
- import { UserPlus, Mail, Lock, Eye, EyeOff } from 'lucide-svelte';
9
+ import * as Alert from './ui/alert/index.js';
10
+ import PasswordInput from './PasswordInput.svelte';
11
+ import { UserPlus, Mail, Loader2, AlertCircle } from 'lucide-svelte';
9
12
 
10
13
  let { title = 'Admin', onSuccess } = $props<{
11
14
  title?: string;
@@ -17,7 +20,6 @@
17
20
  let email = $state('');
18
21
  let password = $state('');
19
22
  let confirmPassword = $state('');
20
- let showPassword = $state(false);
21
23
  let error = $state('');
22
24
 
23
25
  async function handleSubmit(e: SubmitEvent) {
@@ -37,11 +39,11 @@
37
39
  }
38
40
  </script>
39
41
 
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">
42
+ <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/[0.08] via-background to-primary/[0.04] p-4">
43
+ <div class="w-full max-w-[420px]">
44
+ <Card.Card class="backdrop-blur-xl border-border/50 shadow-[0_8px_32px_hsl(var(--primary)/0.08),0_2px_8px_hsl(0_0%_0%/0.06)]">
45
+ <Card.CardHeader class="text-center pb-2">
46
+ <div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mx-auto mb-3">
45
47
  <UserPlus class="h-6 w-6" />
46
48
  </div>
47
49
  <Card.CardTitle class="text-2xl font-bold">{t('auth.createAccount')}</Card.CardTitle>
@@ -50,15 +52,16 @@
50
52
  <Card.CardContent>
51
53
  <form onsubmit={handleSubmit} class="space-y-4">
52
54
  {#if error}
53
- <div class="error-alert">
54
- <p>{error}</p>
55
- </div>
55
+ <Alert.Root variant="destructive">
56
+ <AlertCircle class="h-4 w-4" />
57
+ <Alert.Description>{error}</Alert.Description>
58
+ </Alert.Root>
56
59
  {/if}
57
60
 
58
61
  <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
+ <Label for="register-email">{t('auth.email')}</Label>
63
+ <div class="relative">
64
+ <Mail class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
62
65
  <Input
63
66
  id="register-email"
64
67
  type="email"
@@ -70,63 +73,34 @@
70
73
  </div>
71
74
  </div>
72
75
 
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>
76
+ <PasswordInput
77
+ id="register-password"
78
+ label={t('auth.password')}
79
+ bind:value={password}
80
+ autocomplete="new-password"
81
+ showStrength
82
+ />
83
+
84
+ <PasswordInput
85
+ id="register-confirm"
86
+ label={t('auth.confirmPassword')}
87
+ bind:value={confirmPassword}
88
+ autocomplete="new-password"
89
+ />
114
90
 
115
91
  <Button type="submit" class="w-full" disabled={register.isLoading}>
116
92
  {#if register.isLoading}
117
- <span class="loading-spinner"></span>
93
+ <Loader2 class="h-4 w-4 animate-spin mr-2" />
118
94
  {/if}
119
95
  {t('auth.registerButton')}
120
96
  </Button>
121
97
  </form>
122
98
 
123
- <div class="auth-link">
99
+ <div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
124
100
  <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>
101
+ <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
102
+ {t('auth.login')}
103
+ </Button>
130
104
  </div>
131
105
  </Card.CardContent>
132
106
  </Card.Card>
@@ -136,106 +110,3 @@
136
110
  </p>
137
111
  </div>
138
112
  </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>
@@ -5,7 +5,10 @@
5
5
  import { t, getLocale } from '@svadmin/core/i18n';
6
6
  import { Button } from './ui/button/index.js';
7
7
  import * as Card from './ui/card/index.js';
8
- import { ArrowLeft, Pencil, Loader2 } from 'lucide-svelte';
8
+ import { Skeleton } from './ui/skeleton/index.js';
9
+ import PageHeader from './PageHeader.svelte';
10
+ import { getDisplayComponent } from './fieldComponentMap';
11
+ import { Pencil } from 'lucide-svelte';
9
12
 
10
13
  let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
11
14
 
@@ -13,54 +16,50 @@
13
16
  const showFields = resource.fields.filter(f => f.showInShow !== false);
14
17
 
15
18
  const query = useShow({ resource: resourceName, id });
16
-
17
- function formatValue(field: FieldDefinition, value: unknown): string {
18
- if (value == null) return '—';
19
- if (field.type === 'boolean') return value ? t('common.yes') : t('common.no');
20
- if (field.type === 'date') return new Date(value as string).toLocaleDateString(getLocale());
21
- if (field.type === 'json') return JSON.stringify(value, null, 2);
22
- if (field.type === 'tags' && Array.isArray(value)) return (value as string[]).join(', ');
23
- if (field.type === 'select' && field.options) {
24
- const opt = field.options.find(o => o.value === value);
25
- return opt?.label ?? String(value);
26
- }
27
- return String(value);
28
- }
29
19
  </script>
30
20
 
31
21
  <div class="space-y-6">
32
- <div class="flex items-center justify-between">
33
- <div class="flex items-center gap-4">
22
+ <PageHeader title="{resource.label} {t('common.detail')}">
23
+ {#snippet actions()}
34
24
  <Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
35
- <ArrowLeft class="h-5 w-5" />
36
- </Button>
37
- <h1 class="text-2xl font-bold text-foreground">{resource.label} {t('common.detail')}</h1>
38
- </div>
39
- {#if resource.canEdit !== false}
40
- <Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
41
- <Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
25
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
42
26
  </Button>
43
- {/if}
44
- </div>
27
+ {#if resource.canEdit !== false}
28
+ <Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
29
+ <Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
30
+ </Button>
31
+ {/if}
32
+ {/snippet}
33
+ </PageHeader>
45
34
 
46
- {#if query.isLoading}
47
- <div class="flex h-64 items-center justify-center">
48
- <Loader2 class="h-6 w-6 animate-spin text-primary" />
49
- </div>
50
- {:else if query.data}
35
+ {#if query.query.isLoading}
36
+ <Card.Root>
37
+ <Card.Content class="divide-y divide-border p-0">
38
+ {#each showFields.slice(0, 6) as _}
39
+ <div class="flex px-6 py-4">
40
+ <Skeleton class="h-4 w-1/4" />
41
+ <Skeleton class="h-4 w-2/5 ml-auto" />
42
+ </div>
43
+ {/each}
44
+ </Card.Content>
45
+ </Card.Root>
46
+ {:else if query.query.data?.data}
51
47
  <Card.Root>
52
48
  <Card.Content class="divide-y divide-border p-0">
53
49
  {#each showFields as field}
54
- {@const value = (query.data as Record<string, unknown>)[field.key]}
50
+ {@const value = (query.query.data!.data as Record<string, unknown>)[field.key]}
51
+ {@const DisplayComponent = getDisplayComponent(field.type)}
55
52
  <div class="flex px-6 py-4">
56
53
  <div class="w-1/3 text-sm font-medium text-muted-foreground">{field.label}</div>
57
54
  <div class="w-2/3 text-sm text-foreground">
58
- {#if field.type === 'image' && value}
59
- <img src={value as string} alt={field.label} class="h-20 w-20 rounded-lg object-cover" />
60
- {:else if field.type === 'json' && value}
61
- <pre class="rounded-lg bg-muted p-3 text-xs overflow-auto max-h-40">{formatValue(field, value)}</pre>
55
+ {#if DisplayComponent && value != null}
56
+ <DisplayComponent
57
+ {value}
58
+ options={field.options}
59
+ resourceName={field.resource}
60
+ />
62
61
  {:else}
63
- {formatValue(field, value)}
62
+ {value != null ? String(value) : '—'}
64
63
  {/if}
65
64
  </div>
66
65
  </div>