@svadmin/create 0.32.0 → 0.33.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.
@@ -18,4 +18,4 @@
18
18
  >
19
19
  {#snippet dashboard()}<Dashboard />{/snippet}
20
20
  {#snippet loginPage()}<Login />{/snippet}
21
- </AdminApp>
21
+ </AdminApp>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { useList } from '@svadmin/core';
3
3
  import { posts, users, comments } from '../resource-contracts';
4
- import { Button, DataState, MetricBlock } from '@svadmin/ui';
4
+ import { AutoTable, Button, DashboardPage, DataState, MetricBlock, PageSection } from '@svadmin/ui';
5
5
 
6
6
  const postsQuery = useList({ resource: posts, pagination: { current: 1, pageSize: 1 } });
7
7
  const usersQuery = useList({ resource: users, pagination: { current: 1, pageSize: 1 } });
@@ -15,78 +15,28 @@
15
15
 
16
16
  const hasMetricError = $derived(stats.some(stat => stat.query.isError));
17
17
 
18
- const recentPosts = useList({ resource: posts, pagination: { current: 1, pageSize: 5 } });
19
- const recentUsers = useList({ resource: users, pagination: { current: 1, pageSize: 5 } });
20
18
  </script>
21
19
 
22
- <div class="space-y-4">
23
- <h1 class="text-xl font-semibold text-foreground">Dashboard</h1>
24
-
20
+ <DashboardPage title="Dashboard">
25
21
  {#if hasMetricError}
26
22
  <DataState state="error" title="Some totals are unavailable" retry={() => {
27
23
  for (const stat of stats) if (stat.query.isError && !stat.query.isFetching) void stat.query.refetch();
28
24
  }} />
29
25
  {/if}
30
- <div class="grid gap-4 sm:grid-cols-3">
26
+ {#snippet metrics()}
31
27
  {#each stats as stat (stat.label)}
32
28
  <MetricBlock label={stat.label} value={stat.query.isError ? '—' : stat.query.data?.total ?? '—'} loading={stat.query.isLoading} />
33
29
  {/each}
34
- </div>
35
-
36
- <!-- Recent Data -->
37
- <div class="grid gap-6 lg:grid-cols-2">
38
- <section class="min-w-0">
39
- <div class="flex items-center justify-between border-b py-3">
40
- <h2 class="font-semibold text-foreground">Recent Posts</h2>
41
- <a href="#/posts" class="text-sm text-primary hover:underline">View all</a>
42
- </div>
43
- {#if recentPosts.isLoading}
44
- <DataState state="loading" />
45
- {:else if recentPosts.isError}
46
- <DataState state="error" retry={() => { if (!recentPosts.isFetching) void recentPosts.refetch(); }} />
47
- {:else}
48
- <div class="divide-y">
49
- {#each recentPosts.data?.data ?? [] as post, _i (_i)}
50
- <div class="flex items-center justify-between py-3">
51
- <div class="min-w-0">
52
- <p class="break-words text-sm font-medium text-foreground">{post.title}</p>
53
- <p class="text-xs text-muted-foreground">User #{post.userId}</p>
54
- </div>
55
- </div>
56
- {:else}
57
- <DataState state="empty">
58
- {#snippet action()}<Button href="#/posts/create">Create post</Button>{/snippet}
59
- </DataState>
60
- {/each}
61
- </div>
62
- {/if}
63
- </section>
64
-
65
- <section class="min-w-0">
66
- <div class="flex items-center justify-between border-b py-3">
67
- <h2 class="font-semibold text-foreground">Users</h2>
68
- <a href="#/users" class="text-sm text-primary hover:underline">View all</a>
69
- </div>
70
- {#if recentUsers.isLoading}
71
- <DataState state="loading" />
72
- {:else if recentUsers.isError}
73
- <DataState state="error" retry={() => { if (!recentUsers.isFetching) void recentUsers.refetch(); }} />
74
- {:else}
75
- <div class="divide-y">
76
- {#each recentUsers.data?.data ?? [] as user, _i (_i)}
77
- <div class="flex items-center justify-between py-3">
78
- <div class="min-w-0">
79
- <p class="break-words text-sm font-medium text-foreground">{user.name}</p>
80
- <p class="break-words text-xs text-muted-foreground">{user.email}</p>
81
- </div>
82
- </div>
83
- {:else}
84
- <DataState state="empty">
85
- {#snippet action()}<Button href="#/users">View users</Button>{/snippet}
86
- </DataState>
87
- {/each}
88
- </div>
89
- {/if}
90
- </section>
91
- </div>
92
- </div>
30
+ {/snippet}
31
+
32
+ <PageSection title="Posts">
33
+ {#snippet actions()}<Button variant="ghost" href="#/posts">View all</Button>{/snippet}
34
+ <AutoTable resourceName="posts" showHeader={false} selectable={false} syncWithLocation={false} />
35
+ </PageSection>
36
+ {#snippet secondary()}
37
+ <PageSection title="Users">
38
+ {#snippet actions()}<Button variant="ghost" href="#/users">View all</Button>{/snippet}
39
+ <AutoTable resourceName="users" showHeader={false} selectable={false} syncWithLocation={false} />
40
+ </PageSection>
41
+ {/snippet}
42
+ </DashboardPage>
@@ -28,58 +28,230 @@
28
28
  }
29
29
  </script>
30
30
 
31
- <div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
32
- <div class="w-full max-w-sm space-y-6 rounded-2xl bg-white p-8 shadow-xl shadow-blue-900/5">
31
+ <div class="scaffold-login">
32
+ <div class="scaffold-login__panel">
33
33
  <!-- Logo -->
34
- <div class="text-center">
35
- <div class="mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-primary-50">
36
- <Shield class="h-7 w-7 text-primary" />
34
+ <div class="scaffold-login__header">
35
+ <div class="scaffold-login__mark" aria-hidden="true">
36
+ <Shield size={28} strokeWidth={1.8} />
37
37
  </div>
38
- <h1 class="mt-4 text-xl font-bold text-gray-900">Admin Panel</h1>
39
- <p class="mt-1 text-sm text-gray-500">Sign in to continue</p>
38
+ <h1>Admin Panel</h1>
39
+ <p>Sign in to continue</p>
40
40
  </div>
41
41
 
42
42
  <!-- Error -->
43
43
  {#if error}
44
- <div role="alert" class="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
44
+ <div role="alert" class="scaffold-login__error">
45
45
  {error}
46
46
  </div>
47
47
  {/if}
48
48
 
49
49
  <!-- Form -->
50
- <form onsubmit={handleLogin} class="space-y-4">
51
- <div>
52
- <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
50
+ <form onsubmit={handleLogin} class="scaffold-login__form">
51
+ <label>
52
+ <span>Email</span>
53
53
  <input
54
54
  id="email"
55
55
  type="email"
56
56
  bind:value={email}
57
57
  required
58
- class="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
58
+ class="scaffold-login__input"
59
59
  placeholder="admin@example.com"
60
60
  />
61
- </div>
62
- <div>
63
- <label for="password" class="block text-sm font-medium text-gray-700">Password</label>
61
+ </label>
62
+ <label>
63
+ <span>Password</span>
64
64
  <input
65
65
  id="password"
66
66
  type="password"
67
67
  bind:value={password}
68
68
  required
69
- class="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
70
- placeholder="••••••••"
69
+ class="scaffold-login__input"
70
+ placeholder="Enter your password"
71
71
  />
72
- </div>
72
+ </label>
73
73
  <button
74
74
  type="submit"
75
75
  disabled={loading}
76
- class="flex w-full items-center justify-center gap-2 rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-700 disabled:opacity-50 transition"
76
+ class="scaffold-login__submit"
77
77
  >
78
78
  {#if loading}
79
- <Loader2 class="h-4 w-4 animate-spin" />
79
+ <span class="scaffold-login__spinner" aria-hidden="true"><Loader2 size={16} /></span>
80
80
  {/if}
81
81
  Sign in
82
82
  </button>
83
83
  </form>
84
84
  </div>
85
85
  </div>
86
+
87
+ <style>
88
+ .scaffold-login {
89
+ display: grid;
90
+ min-height: 100dvh;
91
+ place-items: center;
92
+ padding: 2rem 1rem;
93
+ background: var(--background);
94
+ background-image:
95
+ linear-gradient(to right, color-mix(in oklch, var(--primary) 4%, transparent) 1px, transparent 1px),
96
+ linear-gradient(to bottom, color-mix(in oklch, var(--primary) 4%, transparent) 1px, transparent 1px);
97
+ background-size: var(--svadmin-grid-size) var(--svadmin-grid-size);
98
+ color: var(--foreground);
99
+ }
100
+
101
+ .scaffold-login__panel {
102
+ width: min(100%, 28rem);
103
+ border: 1px solid var(--border);
104
+ border-radius: var(--radius-lg);
105
+ background: var(--card);
106
+ padding: 2rem;
107
+ box-shadow: var(--shadow-surface);
108
+ }
109
+
110
+ .scaffold-login__header {
111
+ display: grid;
112
+ justify-items: center;
113
+ gap: 0.5rem;
114
+ text-align: center;
115
+ }
116
+
117
+ .scaffold-login__mark {
118
+ display: grid;
119
+ width: 3.5rem;
120
+ height: 3.5rem;
121
+ place-items: center;
122
+ border: 1px solid color-mix(in oklch, var(--primary) 20%, var(--border));
123
+ border-radius: var(--radius-md);
124
+ background: color-mix(in oklch, var(--primary) 10%, var(--card));
125
+ color: var(--primary);
126
+ }
127
+
128
+ .scaffold-login__header h1 {
129
+ margin: 0.75rem 0 0;
130
+ color: var(--foreground);
131
+ font-size: 1.25rem;
132
+ font-weight: 650;
133
+ line-height: 1.3;
134
+ }
135
+
136
+ .scaffold-login__header p {
137
+ margin: 0;
138
+ color: var(--muted-foreground);
139
+ font-size: 0.875rem;
140
+ }
141
+
142
+ .scaffold-login__error {
143
+ margin-top: 1.5rem;
144
+ border: 1px solid color-mix(in oklch, var(--destructive) 28%, var(--border));
145
+ border-radius: var(--radius-md);
146
+ background: color-mix(in oklch, var(--destructive) 8%, var(--card));
147
+ color: var(--destructive);
148
+ padding: 0.75rem 1rem;
149
+ font-size: 0.875rem;
150
+ }
151
+
152
+ .scaffold-login__form {
153
+ display: grid;
154
+ gap: 1rem;
155
+ margin-top: 2rem;
156
+ }
157
+
158
+ .scaffold-login__form label {
159
+ display: grid;
160
+ gap: 0.5rem;
161
+ color: var(--foreground);
162
+ font-size: 0.875rem;
163
+ font-weight: 600;
164
+ }
165
+
166
+ .scaffold-login__input {
167
+ min-height: 2.75rem;
168
+ width: 100%;
169
+ border: 1px solid var(--border);
170
+ border-radius: var(--radius-md);
171
+ background: var(--card);
172
+ color: var(--foreground);
173
+ padding: 0.625rem 0.75rem;
174
+ font: inherit;
175
+ font-size: 0.875rem;
176
+ box-shadow: var(--shadow-control);
177
+ transition: border-color var(--svadmin-motion-fast) ease-out, box-shadow var(--svadmin-motion-fast) ease-out;
178
+ }
179
+
180
+ .scaffold-login__input::placeholder {
181
+ color: var(--muted-foreground);
182
+ opacity: 0.75;
183
+ }
184
+
185
+ .scaffold-login__input:focus-visible {
186
+ border-color: var(--ring);
187
+ outline: 2px solid color-mix(in oklch, var(--ring) 42%, transparent);
188
+ outline-offset: var(--svadmin-focus-offset);
189
+ }
190
+
191
+ .scaffold-login__submit {
192
+ display: inline-flex;
193
+ min-height: 2.75rem;
194
+ align-items: center;
195
+ justify-content: center;
196
+ gap: 0.5rem;
197
+ width: 100%;
198
+ border: 1px solid color-mix(in oklch, var(--primary) 70%, var(--border));
199
+ border-radius: var(--radius-md);
200
+ background: var(--primary);
201
+ color: var(--primary-foreground);
202
+ padding: 0.625rem 1rem;
203
+ font: inherit;
204
+ font-size: 0.875rem;
205
+ font-weight: 650;
206
+ box-shadow: var(--shadow-control);
207
+ transition: transform var(--svadmin-motion-standard) ease-out, box-shadow var(--svadmin-motion-standard) ease-out, background-color var(--svadmin-motion-standard) ease-out;
208
+ }
209
+
210
+ .scaffold-login__submit:hover:not(:disabled) {
211
+ transform: translateY(-0.125rem);
212
+ }
213
+
214
+ .scaffold-login__submit:active:not(:disabled) {
215
+ transform: translateY(0) scale(0.98);
216
+ box-shadow: var(--shadow-control);
217
+ }
218
+
219
+ .scaffold-login__submit:focus-visible {
220
+ outline: 2px solid color-mix(in oklch, var(--ring) 78%, transparent);
221
+ outline-offset: var(--svadmin-focus-offset);
222
+ }
223
+
224
+ .scaffold-login__submit:disabled {
225
+ cursor: not-allowed;
226
+ opacity: 0.55;
227
+ }
228
+
229
+ .scaffold-login__spinner {
230
+ animation: scaffold-login-spin var(--svadmin-motion-slow) linear infinite;
231
+ }
232
+
233
+ @keyframes scaffold-login-spin {
234
+ to { transform: rotate(360deg); }
235
+ }
236
+
237
+ @media (prefers-reduced-motion: reduce) {
238
+ .scaffold-login__input,
239
+ .scaffold-login__submit {
240
+ transition-duration: 0.01ms;
241
+ }
242
+
243
+ .scaffold-login__spinner {
244
+ animation-duration: 0.01ms;
245
+ }
246
+ }
247
+
248
+ @media (max-width: 32rem) {
249
+ .scaffold-login {
250
+ padding: 1rem;
251
+ }
252
+
253
+ .scaffold-login__panel {
254
+ padding: 1.5rem;
255
+ }
256
+ }
257
+ </style>
@@ -1,10 +1,15 @@
1
1
  import { Type } from '@sinclair/typebox';
2
- import { defineResource } from '@svadmin/core';
2
+ import { defineResource } from '@svadmin/core/resource-contract';
3
3
 
4
+ const postInput = Type.Object({
5
+ userId: Type.Number(), title: Type.String({ minLength: 1 }), body: Type.String(),
6
+ });
4
7
  export const posts = defineResource('posts', {
5
8
  record: Type.Object({
6
9
  id: Type.Number(), userId: Type.Number(), title: Type.String(), body: Type.String(),
7
10
  }),
11
+ create: postInput,
12
+ update: Type.Partial(postInput),
8
13
  });
9
14
  export const users = defineResource('users', {
10
15
  record: Type.Object({
@@ -16,14 +21,28 @@ export const users = defineResource('users', {
16
21
  }),
17
22
  company: Type.Object({ name: Type.String(), catchPhrase: Type.String(), bs: Type.String() }),
18
23
  }),
24
+ update: Type.Partial(Type.Object({
25
+ name: Type.String(), username: Type.String(), email: Type.String(),
26
+ phone: Type.String(), website: Type.String(),
27
+ })),
28
+ });
29
+ const commentInput = Type.Object({
30
+ postId: Type.Number(), name: Type.String({ minLength: 1 }), email: Type.String(), body: Type.String(),
19
31
  });
20
32
  export const comments = defineResource('comments', {
21
33
  record: Type.Object({
22
34
  id: Type.Number(), postId: Type.Number(), name: Type.String(), email: Type.String(), body: Type.String(),
23
35
  }),
36
+ create: commentInput,
37
+ update: Type.Partial(commentInput),
38
+ });
39
+ const todoInput = Type.Object({
40
+ userId: Type.Number(), title: Type.String({ minLength: 1 }), completed: Type.Boolean(),
24
41
  });
25
42
  export const todos = defineResource('todos', {
26
43
  record: Type.Object({
27
44
  id: Type.Number(), userId: Type.Number(), title: Type.String(), completed: Type.Boolean(),
28
45
  }),
46
+ create: todoInput,
47
+ update: Type.Partial(todoInput),
29
48
  });