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.
- package/README.md +172 -0
- package/dist/admin.d.ts +227 -0
- package/dist/admin.js +369 -0
- package/dist/components/AdminForm.svelte +423 -0
- package/dist/components/AdminForm.svelte.d.ts +30 -0
- package/dist/components/AdminLayout.svelte +328 -0
- package/dist/components/AdminLayout.svelte.d.ts +20 -0
- package/dist/components/DataTable.svelte +573 -0
- package/dist/components/DataTable.svelte.d.ts +25 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -0
- package/dist/server/auth/guard.d.ts +36 -0
- package/dist/server/auth/guard.js +38 -0
- package/dist/server/auth/index.d.ts +1 -0
- package/dist/server/auth/index.js +1 -0
- package/dist/server/crud/index.d.ts +1 -0
- package/dist/server/crud/index.js +1 -0
- package/dist/server/crud/operations.d.ts +87 -0
- package/dist/server/crud/operations.js +276 -0
- package/dist/server/introspection/index.d.ts +1 -0
- package/dist/server/introspection/index.js +1 -0
- package/dist/server/introspection/parser.d.ts +51 -0
- package/dist/server/introspection/parser.js +193 -0
- package/package.json +83 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PrismaField } from '../server/introspection/parser.js';
|
|
3
|
+
|
|
4
|
+
interface Column {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
type?: string;
|
|
9
|
+
render?: (value: unknown, row: Record<string, unknown>) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data: Record<string, unknown>[];
|
|
14
|
+
columns: Column[];
|
|
15
|
+
primaryKey?: string;
|
|
16
|
+
basePath: string;
|
|
17
|
+
modelName: string;
|
|
18
|
+
|
|
19
|
+
// Pagination
|
|
20
|
+
page?: number;
|
|
21
|
+
perPage?: number;
|
|
22
|
+
total?: number;
|
|
23
|
+
|
|
24
|
+
// Sorting
|
|
25
|
+
orderBy?: string;
|
|
26
|
+
orderDir?: 'asc' | 'desc';
|
|
27
|
+
|
|
28
|
+
// Search
|
|
29
|
+
searchable?: boolean;
|
|
30
|
+
searchValue?: string;
|
|
31
|
+
|
|
32
|
+
// Actions
|
|
33
|
+
onDelete?: (id: string | number) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
data = [],
|
|
38
|
+
columns = [],
|
|
39
|
+
primaryKey = 'id',
|
|
40
|
+
basePath,
|
|
41
|
+
modelName,
|
|
42
|
+
page = 1,
|
|
43
|
+
perPage = 20,
|
|
44
|
+
total = 0,
|
|
45
|
+
orderBy = 'id',
|
|
46
|
+
orderDir = 'desc',
|
|
47
|
+
searchable = true,
|
|
48
|
+
searchValue = '',
|
|
49
|
+
onDelete
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let searchInput = $state(searchValue);
|
|
53
|
+
let deleteConfirm = $state<string | number | null>(null);
|
|
54
|
+
let isDeleting = $state(false);
|
|
55
|
+
|
|
56
|
+
const totalPages = $derived(Math.ceil(total / perPage));
|
|
57
|
+
const startItem = $derived((page - 1) * perPage + 1);
|
|
58
|
+
const endItem = $derived(Math.min(page * perPage, total));
|
|
59
|
+
|
|
60
|
+
function formatValue(value: unknown, type?: string): string {
|
|
61
|
+
if (value === null || value === undefined) return '—';
|
|
62
|
+
|
|
63
|
+
if (type === 'datetime' || value instanceof Date) {
|
|
64
|
+
return new Date(value as string).toLocaleString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (type === 'boolean' || typeof value === 'boolean') {
|
|
68
|
+
return value ? '✓' : '✗';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
// Handle relations - show a display field
|
|
73
|
+
const obj = value as Record<string, unknown>;
|
|
74
|
+
return String(obj.name || obj.title || obj.email || obj.id || JSON.stringify(value));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return String(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getEditUrl(row: Record<string, unknown>): string {
|
|
81
|
+
return `${basePath}/${modelName.toLowerCase()}/${row[primaryKey]}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleSearch(e: Event) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const url = new URL(window.location.href);
|
|
87
|
+
if (searchInput) {
|
|
88
|
+
url.searchParams.set('search', searchInput);
|
|
89
|
+
} else {
|
|
90
|
+
url.searchParams.delete('search');
|
|
91
|
+
}
|
|
92
|
+
url.searchParams.set('page', '1');
|
|
93
|
+
window.location.href = url.toString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleSort(column: Column) {
|
|
97
|
+
if (!column.sortable) return;
|
|
98
|
+
|
|
99
|
+
const url = new URL(window.location.href);
|
|
100
|
+
const newDir = orderBy === column.key && orderDir === 'asc' ? 'desc' : 'asc';
|
|
101
|
+
url.searchParams.set('orderBy', column.key);
|
|
102
|
+
url.searchParams.set('orderDir', newDir);
|
|
103
|
+
window.location.href = url.toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function goToPage(newPage: number) {
|
|
107
|
+
const url = new URL(window.location.href);
|
|
108
|
+
url.searchParams.set('page', String(newPage));
|
|
109
|
+
window.location.href = url.toString();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleDelete(id: string | number) {
|
|
113
|
+
if (!onDelete) return;
|
|
114
|
+
|
|
115
|
+
isDeleting = true;
|
|
116
|
+
try {
|
|
117
|
+
await onDelete(id);
|
|
118
|
+
deleteConfirm = null;
|
|
119
|
+
window.location.reload();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
alert('Error deleting record');
|
|
122
|
+
} finally {
|
|
123
|
+
isDeleting = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const icons = {
|
|
128
|
+
search: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
|
|
129
|
+
edit: `<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"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>`,
|
|
130
|
+
trash: `<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"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`,
|
|
131
|
+
sortAsc: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></svg>`,
|
|
132
|
+
sortDesc: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/></svg>`,
|
|
133
|
+
chevronLeft: `<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"><path d="m15 18-6-6 6-6"/></svg>`,
|
|
134
|
+
chevronRight: `<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"><path d="m9 18 6-6-6-6"/></svg>`
|
|
135
|
+
};
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<div class="ska-table-container">
|
|
139
|
+
<!-- Toolbar -->
|
|
140
|
+
<div class="ska-table-toolbar">
|
|
141
|
+
{#if searchable}
|
|
142
|
+
<form class="ska-search" onsubmit={handleSearch}>
|
|
143
|
+
<span class="ska-search__icon">{@html icons.search}</span>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
class="ska-search__input"
|
|
147
|
+
placeholder="Search..."
|
|
148
|
+
bind:value={searchInput}
|
|
149
|
+
/>
|
|
150
|
+
</form>
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
153
|
+
<a href="{basePath}/{modelName.toLowerCase()}/new" class="ska-btn ska-btn--primary">
|
|
154
|
+
+ New {modelName}
|
|
155
|
+
</a>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- Table -->
|
|
159
|
+
<div class="ska-table-wrapper">
|
|
160
|
+
<table class="ska-table">
|
|
161
|
+
<thead>
|
|
162
|
+
<tr>
|
|
163
|
+
{#each columns as column}
|
|
164
|
+
<th
|
|
165
|
+
class:ska-table__th--sortable={column.sortable}
|
|
166
|
+
onclick={() => handleSort(column)}
|
|
167
|
+
>
|
|
168
|
+
<span class="ska-table__th-content">
|
|
169
|
+
{column.label}
|
|
170
|
+
{#if column.sortable && orderBy === column.key}
|
|
171
|
+
<span class="ska-table__sort-icon">
|
|
172
|
+
{@html orderDir === 'asc' ? icons.sortAsc : icons.sortDesc}
|
|
173
|
+
</span>
|
|
174
|
+
{/if}
|
|
175
|
+
</span>
|
|
176
|
+
</th>
|
|
177
|
+
{/each}
|
|
178
|
+
<th class="ska-table__th--actions">Actions</th>
|
|
179
|
+
</tr>
|
|
180
|
+
</thead>
|
|
181
|
+
<tbody>
|
|
182
|
+
{#each data as row}
|
|
183
|
+
<tr>
|
|
184
|
+
{#each columns as column}
|
|
185
|
+
<td>
|
|
186
|
+
{#if column.render}
|
|
187
|
+
{@html column.render(row[column.key], row)}
|
|
188
|
+
{:else}
|
|
189
|
+
{formatValue(row[column.key], column.type)}
|
|
190
|
+
{/if}
|
|
191
|
+
</td>
|
|
192
|
+
{/each}
|
|
193
|
+
<td class="ska-table__actions">
|
|
194
|
+
<a href={getEditUrl(row)} class="ska-table__action ska-table__action--edit" title="Edit">
|
|
195
|
+
{@html icons.edit}
|
|
196
|
+
</a>
|
|
197
|
+
{#if onDelete}
|
|
198
|
+
<button
|
|
199
|
+
class="ska-table__action ska-table__action--delete"
|
|
200
|
+
title="Delete"
|
|
201
|
+
onclick={() => deleteConfirm = row[primaryKey] as string | number}
|
|
202
|
+
>
|
|
203
|
+
{@html icons.trash}
|
|
204
|
+
</button>
|
|
205
|
+
{/if}
|
|
206
|
+
</td>
|
|
207
|
+
</tr>
|
|
208
|
+
{:else}
|
|
209
|
+
<tr>
|
|
210
|
+
<td colspan={columns.length + 1} class="ska-table__empty">
|
|
211
|
+
No records found
|
|
212
|
+
</td>
|
|
213
|
+
</tr>
|
|
214
|
+
{/each}
|
|
215
|
+
</tbody>
|
|
216
|
+
</table>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Pagination -->
|
|
220
|
+
{#if totalPages > 1}
|
|
221
|
+
<div class="ska-pagination">
|
|
222
|
+
<span class="ska-pagination__info">
|
|
223
|
+
Showing {startItem} to {endItem} of {total} results
|
|
224
|
+
</span>
|
|
225
|
+
|
|
226
|
+
<div class="ska-pagination__controls">
|
|
227
|
+
<button
|
|
228
|
+
class="ska-pagination__btn"
|
|
229
|
+
disabled={page <= 1}
|
|
230
|
+
onclick={() => goToPage(page - 1)}
|
|
231
|
+
>
|
|
232
|
+
{@html icons.chevronLeft}
|
|
233
|
+
</button>
|
|
234
|
+
|
|
235
|
+
{#each Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
236
|
+
// Show pages around current page
|
|
237
|
+
let p;
|
|
238
|
+
if (totalPages <= 5) {
|
|
239
|
+
p = i + 1;
|
|
240
|
+
} else if (page <= 3) {
|
|
241
|
+
p = i + 1;
|
|
242
|
+
} else if (page >= totalPages - 2) {
|
|
243
|
+
p = totalPages - 4 + i;
|
|
244
|
+
} else {
|
|
245
|
+
p = page - 2 + i;
|
|
246
|
+
}
|
|
247
|
+
return p;
|
|
248
|
+
}) as pageNum}
|
|
249
|
+
<button
|
|
250
|
+
class="ska-pagination__btn"
|
|
251
|
+
class:ska-pagination__btn--active={pageNum === page}
|
|
252
|
+
onclick={() => goToPage(pageNum)}
|
|
253
|
+
>
|
|
254
|
+
{pageNum}
|
|
255
|
+
</button>
|
|
256
|
+
{/each}
|
|
257
|
+
|
|
258
|
+
<button
|
|
259
|
+
class="ska-pagination__btn"
|
|
260
|
+
disabled={page >= totalPages}
|
|
261
|
+
onclick={() => goToPage(page + 1)}
|
|
262
|
+
>
|
|
263
|
+
{@html icons.chevronRight}
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Delete Confirmation Modal -->
|
|
271
|
+
{#if deleteConfirm !== null}
|
|
272
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
273
|
+
<div class="ska-modal-overlay" role="presentation" onclick={() => deleteConfirm = null}>
|
|
274
|
+
<div class="ska-modal" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
|
|
275
|
+
<h3 class="ska-modal__title">Confirm Delete</h3>
|
|
276
|
+
<p class="ska-modal__text">Are you sure you want to delete this record? This action cannot be undone.</p>
|
|
277
|
+
<div class="ska-modal__actions">
|
|
278
|
+
<button class="ska-btn ska-btn--secondary" onclick={() => deleteConfirm = null}>
|
|
279
|
+
Cancel
|
|
280
|
+
</button>
|
|
281
|
+
<button
|
|
282
|
+
class="ska-btn ska-btn--danger"
|
|
283
|
+
onclick={() => handleDelete(deleteConfirm!)}
|
|
284
|
+
disabled={isDeleting}
|
|
285
|
+
>
|
|
286
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
{/if}
|
|
292
|
+
|
|
293
|
+
<style>
|
|
294
|
+
.ska-table-container {
|
|
295
|
+
background: white;
|
|
296
|
+
border-radius: 0.5rem;
|
|
297
|
+
border: 1px solid #e2e8f0;
|
|
298
|
+
overflow: hidden;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.ska-table-toolbar {
|
|
302
|
+
display: flex;
|
|
303
|
+
justify-content: space-between;
|
|
304
|
+
align-items: center;
|
|
305
|
+
padding: 1rem;
|
|
306
|
+
border-bottom: 1px solid #e2e8f0;
|
|
307
|
+
gap: 1rem;
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.ska-search {
|
|
312
|
+
position: relative;
|
|
313
|
+
flex: 1;
|
|
314
|
+
max-width: 20rem;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.ska-search__icon {
|
|
318
|
+
position: absolute;
|
|
319
|
+
left: 0.75rem;
|
|
320
|
+
top: 50%;
|
|
321
|
+
transform: translateY(-50%);
|
|
322
|
+
color: #94a3b8;
|
|
323
|
+
pointer-events: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.ska-search__input {
|
|
327
|
+
width: 100%;
|
|
328
|
+
padding: 0.5rem 0.75rem 0.5rem 2.5rem;
|
|
329
|
+
border: 1px solid #e2e8f0;
|
|
330
|
+
border-radius: 0.375rem;
|
|
331
|
+
font-size: 0.875rem;
|
|
332
|
+
outline: none;
|
|
333
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.ska-search__input:focus {
|
|
337
|
+
border-color: var(--ska-primary, #6366f1);
|
|
338
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.ska-btn {
|
|
342
|
+
display: inline-flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 0.5rem;
|
|
345
|
+
padding: 0.5rem 1rem;
|
|
346
|
+
border-radius: 0.375rem;
|
|
347
|
+
font-size: 0.875rem;
|
|
348
|
+
font-weight: 500;
|
|
349
|
+
text-decoration: none;
|
|
350
|
+
border: none;
|
|
351
|
+
cursor: pointer;
|
|
352
|
+
transition: all 0.15s;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.ska-btn--primary {
|
|
356
|
+
background: var(--ska-primary, #6366f1);
|
|
357
|
+
color: white;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.ska-btn--primary:hover {
|
|
361
|
+
opacity: 0.9;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.ska-btn--secondary {
|
|
365
|
+
background: #f1f5f9;
|
|
366
|
+
color: #475569;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.ska-btn--secondary:hover {
|
|
370
|
+
background: #e2e8f0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.ska-btn--danger {
|
|
374
|
+
background: #ef4444;
|
|
375
|
+
color: white;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.ska-btn--danger:hover {
|
|
379
|
+
background: #dc2626;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.ska-btn:disabled {
|
|
383
|
+
opacity: 0.5;
|
|
384
|
+
cursor: not-allowed;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.ska-table-wrapper {
|
|
388
|
+
overflow-x: auto;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.ska-table {
|
|
392
|
+
width: 100%;
|
|
393
|
+
border-collapse: collapse;
|
|
394
|
+
font-size: 0.875rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.ska-table th {
|
|
398
|
+
text-align: left;
|
|
399
|
+
padding: 0.75rem 1rem;
|
|
400
|
+
background: #f8fafc;
|
|
401
|
+
font-weight: 600;
|
|
402
|
+
color: #475569;
|
|
403
|
+
border-bottom: 1px solid #e2e8f0;
|
|
404
|
+
white-space: nowrap;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.ska-table__th--sortable {
|
|
408
|
+
cursor: pointer;
|
|
409
|
+
user-select: none;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.ska-table__th--sortable:hover {
|
|
413
|
+
background: #f1f5f9;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.ska-table__th-content {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 0.375rem;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.ska-table__sort-icon {
|
|
423
|
+
color: var(--ska-primary, #6366f1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.ska-table__th--actions {
|
|
427
|
+
width: 100px;
|
|
428
|
+
text-align: center;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.ska-table td {
|
|
432
|
+
padding: 0.75rem 1rem;
|
|
433
|
+
border-bottom: 1px solid #f1f5f9;
|
|
434
|
+
color: #1e293b;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.ska-table tbody tr:hover {
|
|
438
|
+
background: #f8fafc;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.ska-table__actions {
|
|
442
|
+
display: flex;
|
|
443
|
+
align-items: center;
|
|
444
|
+
justify-content: center;
|
|
445
|
+
gap: 0.5rem;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.ska-table__action {
|
|
449
|
+
display: flex;
|
|
450
|
+
align-items: center;
|
|
451
|
+
justify-content: center;
|
|
452
|
+
width: 1.75rem;
|
|
453
|
+
height: 1.75rem;
|
|
454
|
+
border-radius: 0.25rem;
|
|
455
|
+
border: none;
|
|
456
|
+
background: transparent;
|
|
457
|
+
cursor: pointer;
|
|
458
|
+
transition: all 0.15s;
|
|
459
|
+
text-decoration: none;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.ska-table__action--edit {
|
|
463
|
+
color: #6366f1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.ska-table__action--edit:hover {
|
|
467
|
+
background: #eef2ff;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.ska-table__action--delete {
|
|
471
|
+
color: #ef4444;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.ska-table__action--delete:hover {
|
|
475
|
+
background: #fef2f2;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.ska-table__empty {
|
|
479
|
+
text-align: center;
|
|
480
|
+
color: #94a3b8;
|
|
481
|
+
padding: 3rem 1rem !important;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.ska-pagination {
|
|
485
|
+
display: flex;
|
|
486
|
+
justify-content: space-between;
|
|
487
|
+
align-items: center;
|
|
488
|
+
padding: 0.75rem 1rem;
|
|
489
|
+
border-top: 1px solid #e2e8f0;
|
|
490
|
+
flex-wrap: wrap;
|
|
491
|
+
gap: 1rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.ska-pagination__info {
|
|
495
|
+
font-size: 0.875rem;
|
|
496
|
+
color: #64748b;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.ska-pagination__controls {
|
|
500
|
+
display: flex;
|
|
501
|
+
gap: 0.25rem;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.ska-pagination__btn {
|
|
505
|
+
display: flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
justify-content: center;
|
|
508
|
+
min-width: 2rem;
|
|
509
|
+
height: 2rem;
|
|
510
|
+
padding: 0 0.5rem;
|
|
511
|
+
border: 1px solid #e2e8f0;
|
|
512
|
+
border-radius: 0.25rem;
|
|
513
|
+
background: white;
|
|
514
|
+
font-size: 0.875rem;
|
|
515
|
+
color: #475569;
|
|
516
|
+
cursor: pointer;
|
|
517
|
+
transition: all 0.15s;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.ska-pagination__btn:hover:not(:disabled) {
|
|
521
|
+
background: #f8fafc;
|
|
522
|
+
border-color: #cbd5e1;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.ska-pagination__btn--active {
|
|
526
|
+
background: var(--ska-primary, #6366f1);
|
|
527
|
+
border-color: var(--ska-primary, #6366f1);
|
|
528
|
+
color: white;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.ska-pagination__btn:disabled {
|
|
532
|
+
opacity: 0.5;
|
|
533
|
+
cursor: not-allowed;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.ska-modal-overlay {
|
|
537
|
+
position: fixed;
|
|
538
|
+
inset: 0;
|
|
539
|
+
background: rgba(0, 0, 0, 0.5);
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
justify-content: center;
|
|
543
|
+
z-index: 50;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.ska-modal {
|
|
547
|
+
background: white;
|
|
548
|
+
border-radius: 0.5rem;
|
|
549
|
+
padding: 1.5rem;
|
|
550
|
+
max-width: 24rem;
|
|
551
|
+
width: 90%;
|
|
552
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.ska-modal__title {
|
|
556
|
+
font-size: 1.125rem;
|
|
557
|
+
font-weight: 600;
|
|
558
|
+
color: #1e293b;
|
|
559
|
+
margin: 0 0 0.5rem;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.ska-modal__text {
|
|
563
|
+
color: #64748b;
|
|
564
|
+
font-size: 0.875rem;
|
|
565
|
+
margin: 0 0 1.5rem;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.ska-modal__actions {
|
|
569
|
+
display: flex;
|
|
570
|
+
justify-content: flex-end;
|
|
571
|
+
gap: 0.75rem;
|
|
572
|
+
}
|
|
573
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface Column {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
sortable?: boolean;
|
|
5
|
+
type?: string;
|
|
6
|
+
render?: (value: unknown, row: Record<string, unknown>) => string;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
data: Record<string, unknown>[];
|
|
10
|
+
columns: Column[];
|
|
11
|
+
primaryKey?: string;
|
|
12
|
+
basePath: string;
|
|
13
|
+
modelName: string;
|
|
14
|
+
page?: number;
|
|
15
|
+
perPage?: number;
|
|
16
|
+
total?: number;
|
|
17
|
+
orderBy?: string;
|
|
18
|
+
orderDir?: 'asc' | 'desc';
|
|
19
|
+
searchable?: boolean;
|
|
20
|
+
searchValue?: string;
|
|
21
|
+
onDelete?: (id: string | number) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
declare const DataTable: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type DataTable = ReturnType<typeof DataTable>;
|
|
25
|
+
export default DataTable;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit Admin
|
|
3
|
+
* Django-like admin panel for SvelteKit + Prisma
|
|
4
|
+
*/
|
|
5
|
+
export { createAdmin, createLayoutLoad, createDashboardLoad, createModelListLoad, createModelNewLoad, createModelNewAction, createModelEditLoad, createModelEditAction, createModelDeleteAction, createAdminGuard, type AdminConfig, type AdminContext } from './admin.js';
|
|
6
|
+
export { parsePrismaSchema, parseSchemaContent, getDisplayFields, getEditableFields, getInputType, fieldToLabel, type PrismaSchema, type PrismaModel, type PrismaField } from './server/introspection/parser.js';
|
|
7
|
+
export { createListOperation, createGetOperation, createCreateOperation, createUpdateOperation, createDeleteOperation, buildSearchWhere, buildFilterWhere, type ListOptions, type ListResult } from './server/crud/operations.js';
|
|
8
|
+
export { createAuthGuard, defaultAdminCheck, type AdminAuthConfig, type AdminSession } from './server/auth/guard.js';
|
|
9
|
+
export { default as AdminLayout } from './components/AdminLayout.svelte';
|
|
10
|
+
export { default as DataTable } from './components/DataTable.svelte';
|
|
11
|
+
export { default as AdminForm } from './components/AdminForm.svelte';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit Admin
|
|
3
|
+
* Django-like admin panel for SvelteKit + Prisma
|
|
4
|
+
*/
|
|
5
|
+
// Core admin factory and loaders
|
|
6
|
+
export { createAdmin, createLayoutLoad, createDashboardLoad, createModelListLoad, createModelNewLoad, createModelNewAction, createModelEditLoad, createModelEditAction, createModelDeleteAction, createAdminGuard } from './admin.js';
|
|
7
|
+
// Prisma introspection utilities
|
|
8
|
+
export { parsePrismaSchema, parseSchemaContent, getDisplayFields, getEditableFields, getInputType, fieldToLabel } from './server/introspection/parser.js';
|
|
9
|
+
// CRUD operations
|
|
10
|
+
export { createListOperation, createGetOperation, createCreateOperation, createUpdateOperation, createDeleteOperation, buildSearchWhere, buildFilterWhere } from './server/crud/operations.js';
|
|
11
|
+
// Auth utilities
|
|
12
|
+
export { createAuthGuard, defaultAdminCheck } from './server/auth/guard.js';
|
|
13
|
+
// Re-export components
|
|
14
|
+
export { default as AdminLayout } from './components/AdminLayout.svelte';
|
|
15
|
+
export { default as DataTable } from './components/DataTable.svelte';
|
|
16
|
+
export { default as AdminForm } from './components/AdminForm.svelte';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-auth integration for admin panel
|
|
3
|
+
*/
|
|
4
|
+
export interface AdminAuthConfig {
|
|
5
|
+
provider: 'better-auth';
|
|
6
|
+
adminRole?: string;
|
|
7
|
+
adminCheck?: (user: unknown) => boolean | Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Default admin check - looks for role field
|
|
11
|
+
*/
|
|
12
|
+
export declare function defaultAdminCheck(user: unknown, adminRole?: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Create auth guard for server routes
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAuthGuard(config: AdminAuthConfig): (locals: Record<string, unknown>) => Promise<{
|
|
17
|
+
authorized: boolean;
|
|
18
|
+
user: unknown | null;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Type for admin session
|
|
22
|
+
*/
|
|
23
|
+
export interface AdminSession {
|
|
24
|
+
user: {
|
|
25
|
+
id: string;
|
|
26
|
+
email: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
role?: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
session: {
|
|
32
|
+
id: string;
|
|
33
|
+
expiresAt: Date;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
}
|