sveltekit-admin 0.1.0

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,423 @@
1
+ <script lang="ts">
2
+ import type { PrismaField } from '../server/introspection/parser.js';
3
+ import { getInputType, fieldToLabel } from '../server/introspection/parser.js';
4
+
5
+ interface FieldConfig {
6
+ name: string;
7
+ type: string;
8
+ label?: string;
9
+ required?: boolean;
10
+ readonly?: boolean;
11
+ hidden?: boolean;
12
+ placeholder?: string;
13
+ options?: { value: string; label: string }[];
14
+ relationOptions?: { id: string | number; label: string }[];
15
+ }
16
+
17
+ interface Props {
18
+ fields: FieldConfig[];
19
+ values?: Record<string, unknown>;
20
+ action: string;
21
+ method?: 'GET' | 'POST';
22
+ submitLabel?: string;
23
+ cancelHref?: string;
24
+ errors?: Record<string, string>;
25
+ onSubmit?: (data: Record<string, unknown>) => Promise<void>;
26
+ }
27
+
28
+ let {
29
+ fields = [],
30
+ values = {},
31
+ action,
32
+ method = 'POST',
33
+ submitLabel = 'Save',
34
+ cancelHref,
35
+ errors = {},
36
+ onSubmit
37
+ }: Props = $props();
38
+
39
+ let formValues = $state<Record<string, unknown>>({ ...values });
40
+ let isSubmitting = $state(false);
41
+ let formErrors = $state<Record<string, string>>({ ...errors });
42
+
43
+ function getFieldValue(name: string): unknown {
44
+ return formValues[name] ?? values[name] ?? '';
45
+ }
46
+
47
+ function setFieldValue(name: string, value: unknown) {
48
+ formValues[name] = value;
49
+ // Clear error when user types
50
+ if (formErrors[name]) {
51
+ delete formErrors[name];
52
+ }
53
+ }
54
+
55
+ async function handleSubmit(e: Event) {
56
+ if (!onSubmit) return;
57
+
58
+ e.preventDefault();
59
+ isSubmitting = true;
60
+ formErrors = {};
61
+
62
+ try {
63
+ await onSubmit(formValues);
64
+ } catch (err: unknown) {
65
+ if (err && typeof err === 'object' && 'fieldErrors' in err) {
66
+ formErrors = err.fieldErrors as Record<string, string>;
67
+ } else {
68
+ formErrors = { _form: err instanceof Error ? err.message : 'An error occurred' };
69
+ }
70
+ } finally {
71
+ isSubmitting = false;
72
+ }
73
+ }
74
+
75
+ function formatDateTimeLocal(value: unknown): string {
76
+ if (!value) return '';
77
+ const date = new Date(value as string);
78
+ return date.toISOString().slice(0, 16);
79
+ }
80
+
81
+ const icons = {
82
+ calendar: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>`
83
+ };
84
+ </script>
85
+
86
+ <form
87
+ class="ska-form"
88
+ {action}
89
+ {method}
90
+ onsubmit={onSubmit ? handleSubmit : undefined}
91
+ >
92
+ {#if formErrors._form}
93
+ <div class="ska-form__error-banner">
94
+ {formErrors._form}
95
+ </div>
96
+ {/if}
97
+
98
+ <div class="ska-form__fields">
99
+ {#each fields.filter(f => !f.hidden) as field}
100
+ <div class="ska-form__group" class:ska-form__group--error={formErrors[field.name]}>
101
+ <label class="ska-form__label" for={field.name}>
102
+ {field.label || fieldToLabel(field.name)}
103
+ {#if field.required}
104
+ <span class="ska-form__required">*</span>
105
+ {/if}
106
+ </label>
107
+
108
+ {#if field.readonly}
109
+ <div class="ska-form__static">
110
+ {getFieldValue(field.name) || '—'}
111
+ </div>
112
+ {:else if field.type === 'textarea'}
113
+ <textarea
114
+ id={field.name}
115
+ name={field.name}
116
+ class="ska-form__input ska-form__textarea"
117
+ placeholder={field.placeholder}
118
+ required={field.required}
119
+ value={String(getFieldValue(field.name) || '')}
120
+ oninput={(e) => setFieldValue(field.name, e.currentTarget.value)}
121
+ ></textarea>
122
+ {:else if field.type === 'checkbox'}
123
+ <label class="ska-form__checkbox">
124
+ <input
125
+ type="checkbox"
126
+ id={field.name}
127
+ name={field.name}
128
+ checked={Boolean(getFieldValue(field.name))}
129
+ onchange={(e) => setFieldValue(field.name, e.currentTarget.checked)}
130
+ />
131
+ <span class="ska-form__checkbox-mark"></span>
132
+ <span class="ska-form__checkbox-label">Enabled</span>
133
+ </label>
134
+ {:else if field.type === 'select' || field.options}
135
+ <select
136
+ id={field.name}
137
+ name={field.name}
138
+ class="ska-form__input ska-form__select"
139
+ required={field.required}
140
+ value={String(getFieldValue(field.name) || '')}
141
+ onchange={(e) => setFieldValue(field.name, e.currentTarget.value)}
142
+ >
143
+ <option value="">Select...</option>
144
+ {#each field.options || [] as option}
145
+ <option value={option.value}>{option.label}</option>
146
+ {/each}
147
+ </select>
148
+ {:else if field.type === 'relation' && field.relationOptions}
149
+ <select
150
+ id={field.name}
151
+ name={field.name}
152
+ class="ska-form__input ska-form__select"
153
+ required={field.required}
154
+ value={String(getFieldValue(field.name) || '')}
155
+ onchange={(e) => setFieldValue(field.name, e.currentTarget.value)}
156
+ >
157
+ <option value="">Select...</option>
158
+ {#each field.relationOptions as option}
159
+ <option value={option.id}>{option.label}</option>
160
+ {/each}
161
+ </select>
162
+ {:else if field.type === 'datetime'}
163
+ <input
164
+ type="datetime-local"
165
+ id={field.name}
166
+ name={field.name}
167
+ class="ska-form__input"
168
+ required={field.required}
169
+ value={formatDateTimeLocal(getFieldValue(field.name))}
170
+ onchange={(e) => setFieldValue(field.name, e.currentTarget.value)}
171
+ />
172
+ {:else if field.type === 'number'}
173
+ <input
174
+ type="number"
175
+ id={field.name}
176
+ name={field.name}
177
+ class="ska-form__input"
178
+ placeholder={field.placeholder}
179
+ required={field.required}
180
+ value={String(getFieldValue(field.name) || '')}
181
+ oninput={(e) => setFieldValue(field.name, e.currentTarget.value)}
182
+ />
183
+ {:else if field.type === 'json'}
184
+ <textarea
185
+ id={field.name}
186
+ name={field.name}
187
+ class="ska-form__input ska-form__textarea ska-form__json"
188
+ placeholder={'{"key": "value"}'}
189
+ required={field.required}
190
+ value={typeof getFieldValue(field.name) === 'object'
191
+ ? JSON.stringify(getFieldValue(field.name), null, 2)
192
+ : String(getFieldValue(field.name) || '')}
193
+ oninput={(e) => setFieldValue(field.name, e.currentTarget.value)}
194
+ ></textarea>
195
+ {:else}
196
+ <input
197
+ type={field.type === 'email' ? 'email' : field.type === 'password' ? 'password' : field.type === 'url' ? 'url' : 'text'}
198
+ id={field.name}
199
+ name={field.name}
200
+ class="ska-form__input"
201
+ placeholder={field.placeholder}
202
+ required={field.required}
203
+ value={String(getFieldValue(field.name) || '')}
204
+ oninput={(e) => setFieldValue(field.name, e.currentTarget.value)}
205
+ />
206
+ {/if}
207
+
208
+ {#if formErrors[field.name]}
209
+ <span class="ska-form__error">{formErrors[field.name]}</span>
210
+ {/if}
211
+ </div>
212
+ {/each}
213
+ </div>
214
+
215
+ <div class="ska-form__actions">
216
+ {#if cancelHref}
217
+ <a href={cancelHref} class="ska-btn ska-btn--secondary">Cancel</a>
218
+ {/if}
219
+ <button type="submit" class="ska-btn ska-btn--primary" disabled={isSubmitting}>
220
+ {isSubmitting ? 'Saving...' : submitLabel}
221
+ </button>
222
+ </div>
223
+ </form>
224
+
225
+ <style>
226
+ .ska-form {
227
+ background: white;
228
+ border-radius: 0.5rem;
229
+ border: 1px solid #e2e8f0;
230
+ padding: 1.5rem;
231
+ }
232
+
233
+ .ska-form__error-banner {
234
+ background: #fef2f2;
235
+ border: 1px solid #fecaca;
236
+ color: #dc2626;
237
+ padding: 0.75rem 1rem;
238
+ border-radius: 0.375rem;
239
+ margin-bottom: 1.5rem;
240
+ font-size: 0.875rem;
241
+ }
242
+
243
+ .ska-form__fields {
244
+ display: grid;
245
+ gap: 1.25rem;
246
+ }
247
+
248
+ .ska-form__group {
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 0.375rem;
252
+ }
253
+
254
+ .ska-form__group--error .ska-form__input {
255
+ border-color: #ef4444;
256
+ }
257
+
258
+ .ska-form__label {
259
+ font-size: 0.875rem;
260
+ font-weight: 500;
261
+ color: #374151;
262
+ }
263
+
264
+ .ska-form__required {
265
+ color: #ef4444;
266
+ margin-left: 0.125rem;
267
+ }
268
+
269
+ .ska-form__input {
270
+ padding: 0.625rem 0.75rem;
271
+ border: 1px solid #e2e8f0;
272
+ border-radius: 0.375rem;
273
+ font-size: 0.875rem;
274
+ outline: none;
275
+ transition: border-color 0.15s, box-shadow 0.15s;
276
+ background: white;
277
+ color: #1e293b;
278
+ }
279
+
280
+ .ska-form__input:focus {
281
+ border-color: var(--ska-primary, #6366f1);
282
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
283
+ }
284
+
285
+ .ska-form__input::placeholder {
286
+ color: #94a3b8;
287
+ }
288
+
289
+ .ska-form__textarea {
290
+ min-height: 6rem;
291
+ resize: vertical;
292
+ font-family: inherit;
293
+ }
294
+
295
+ .ska-form__json {
296
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
297
+ font-size: 0.8125rem;
298
+ }
299
+
300
+ .ska-form__select {
301
+ appearance: none;
302
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
303
+ background-position: right 0.5rem center;
304
+ background-repeat: no-repeat;
305
+ background-size: 1.5em 1.5em;
306
+ padding-right: 2.5rem;
307
+ }
308
+
309
+ .ska-form__static {
310
+ padding: 0.625rem 0;
311
+ font-size: 0.875rem;
312
+ color: #64748b;
313
+ }
314
+
315
+ .ska-form__checkbox {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 0.5rem;
319
+ cursor: pointer;
320
+ user-select: none;
321
+ }
322
+
323
+ .ska-form__checkbox input {
324
+ position: absolute;
325
+ opacity: 0;
326
+ width: 0;
327
+ height: 0;
328
+ }
329
+
330
+ .ska-form__checkbox-mark {
331
+ width: 1.25rem;
332
+ height: 1.25rem;
333
+ border: 1px solid #e2e8f0;
334
+ border-radius: 0.25rem;
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ transition: all 0.15s;
339
+ }
340
+
341
+ .ska-form__checkbox input:checked + .ska-form__checkbox-mark {
342
+ background: var(--ska-primary, #6366f1);
343
+ border-color: var(--ska-primary, #6366f1);
344
+ }
345
+
346
+ .ska-form__checkbox input:checked + .ska-form__checkbox-mark::after {
347
+ content: '';
348
+ width: 0.5rem;
349
+ height: 0.5rem;
350
+ background: white;
351
+ border-radius: 0.125rem;
352
+ }
353
+
354
+ .ska-form__checkbox input:focus + .ska-form__checkbox-mark {
355
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
356
+ }
357
+
358
+ .ska-form__checkbox-label {
359
+ font-size: 0.875rem;
360
+ color: #475569;
361
+ }
362
+
363
+ .ska-form__error {
364
+ font-size: 0.75rem;
365
+ color: #ef4444;
366
+ }
367
+
368
+ .ska-form__actions {
369
+ display: flex;
370
+ justify-content: flex-end;
371
+ gap: 0.75rem;
372
+ margin-top: 1.5rem;
373
+ padding-top: 1.5rem;
374
+ border-top: 1px solid #e2e8f0;
375
+ }
376
+
377
+ .ska-btn {
378
+ display: inline-flex;
379
+ align-items: center;
380
+ gap: 0.5rem;
381
+ padding: 0.625rem 1.25rem;
382
+ border-radius: 0.375rem;
383
+ font-size: 0.875rem;
384
+ font-weight: 500;
385
+ text-decoration: none;
386
+ border: none;
387
+ cursor: pointer;
388
+ transition: all 0.15s;
389
+ }
390
+
391
+ .ska-btn--primary {
392
+ background: var(--ska-primary, #6366f1);
393
+ color: white;
394
+ }
395
+
396
+ .ska-btn--primary:hover:not(:disabled) {
397
+ opacity: 0.9;
398
+ }
399
+
400
+ .ska-btn--secondary {
401
+ background: #f1f5f9;
402
+ color: #475569;
403
+ }
404
+
405
+ .ska-btn--secondary:hover {
406
+ background: #e2e8f0;
407
+ }
408
+
409
+ .ska-btn:disabled {
410
+ opacity: 0.5;
411
+ cursor: not-allowed;
412
+ }
413
+
414
+ @media (min-width: 640px) {
415
+ .ska-form__fields {
416
+ grid-template-columns: repeat(2, 1fr);
417
+ }
418
+
419
+ .ska-form__group:has(.ska-form__textarea) {
420
+ grid-column: span 2;
421
+ }
422
+ }
423
+ </style>
@@ -0,0 +1,30 @@
1
+ interface FieldConfig {
2
+ name: string;
3
+ type: string;
4
+ label?: string;
5
+ required?: boolean;
6
+ readonly?: boolean;
7
+ hidden?: boolean;
8
+ placeholder?: string;
9
+ options?: {
10
+ value: string;
11
+ label: string;
12
+ }[];
13
+ relationOptions?: {
14
+ id: string | number;
15
+ label: string;
16
+ }[];
17
+ }
18
+ interface Props {
19
+ fields: FieldConfig[];
20
+ values?: Record<string, unknown>;
21
+ action: string;
22
+ method?: 'GET' | 'POST';
23
+ submitLabel?: string;
24
+ cancelHref?: string;
25
+ errors?: Record<string, string>;
26
+ onSubmit?: (data: Record<string, unknown>) => Promise<void>;
27
+ }
28
+ declare const AdminForm: import("svelte").Component<Props, {}, "">;
29
+ type AdminForm = ReturnType<typeof AdminForm>;
30
+ export default AdminForm;