@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.
- package/dist/index.js +26 -8
- package/guidance/AGENTS.md +23 -0
- package/guidance/DESIGN.md +113 -27
- package/package.json +2 -2
- package/scaffold-manifest.json +33 -22
- package/template/patches/@refinedev%2Fcore@5.0.12.patch +30 -0
- package/template/patches/bits-ui@2.19.0.patch +910 -0
- package/template/src/App.svelte +1 -1
- package/template/src/pages/Dashboard.svelte +16 -66
- package/template/src/pages/Login.svelte +192 -20
- package/template/src/resource-contracts.ts +20 -1
package/template/src/App.svelte
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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="
|
|
32
|
-
<div class="
|
|
31
|
+
<div class="scaffold-login">
|
|
32
|
+
<div class="scaffold-login__panel">
|
|
33
33
|
<!-- Logo -->
|
|
34
|
-
<div class="
|
|
35
|
-
<div class="
|
|
36
|
-
<Shield
|
|
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
|
|
39
|
-
<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="
|
|
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="
|
|
51
|
-
<
|
|
52
|
-
<
|
|
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="
|
|
58
|
+
class="scaffold-login__input"
|
|
59
59
|
placeholder="admin@example.com"
|
|
60
60
|
/>
|
|
61
|
-
</
|
|
62
|
-
<
|
|
63
|
-
<
|
|
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="
|
|
70
|
-
placeholder="
|
|
69
|
+
class="scaffold-login__input"
|
|
70
|
+
placeholder="Enter your password"
|
|
71
71
|
/>
|
|
72
|
-
</
|
|
72
|
+
</label>
|
|
73
73
|
<button
|
|
74
74
|
type="submit"
|
|
75
75
|
disabled={loading}
|
|
76
|
-
class="
|
|
76
|
+
class="scaffold-login__submit"
|
|
77
77
|
>
|
|
78
78
|
{#if loading}
|
|
79
|
-
<
|
|
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
|
});
|