@svadmin/ui 0.32.1 → 0.32.4
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/components/AdminApp.svelte +2 -2
- package/dist/components/AuditLogViewer.svelte +5 -7
- package/dist/components/AutoForm.svelte +3 -1
- package/dist/components/AutoForm.svelte.d.ts +1 -0
- package/dist/components/AutoTable.svelte +10 -10
- package/dist/components/Breadcrumbs.svelte +1 -1
- package/dist/components/ChatDialog.svelte +47 -15
- package/dist/components/ComboboxField.svelte +11 -1
- package/dist/components/ConfirmDialog.svelte +1 -1
- package/dist/components/DrawerForm.svelte +2 -2
- package/dist/components/FieldRenderer.svelte +18 -4
- package/dist/components/InlineEdit.svelte +14 -4
- package/dist/components/Layout.svelte +7 -3
- package/dist/components/ModalForm.svelte +2 -2
- package/dist/components/SidebarItem.svelte +1 -2
- package/dist/components/StepsForm.svelte +1 -0
- package/dist/components/UndoableNotification.svelte +5 -7
- package/dist/components/fields/RelationField.svelte +7 -1
- package/package.json +18 -8
- package/src/app.css +674 -0
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
|
|
92
92
|
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
const resolvedRouter = $derived(routerProvider ??
|
|
94
|
+
const defaultRouter = createHashRouterProvider();
|
|
95
|
+
const resolvedRouter = $derived(routerProvider ?? defaultRouter);
|
|
96
96
|
const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
|
|
97
97
|
|
|
98
98
|
// Set up context — use $effect.pre so prop changes are tracked and children can access during first render
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
let drawerOpen = $state(false);
|
|
24
24
|
let drawerLog = $state<AuditLog | null>(null);
|
|
25
25
|
|
|
26
|
-
let
|
|
26
|
+
let requestId = 0;
|
|
27
27
|
|
|
28
28
|
async function loadLogs() {
|
|
29
29
|
if (!authProvider?.getAuditLogs) {
|
|
@@ -31,27 +31,25 @@
|
|
|
31
31
|
loading = false;
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
const currentId = ++requestId;
|
|
34
35
|
loading = true;
|
|
35
36
|
error = null;
|
|
36
37
|
try {
|
|
37
38
|
const result = await authProvider.getAuditLogs({ page, pageSize });
|
|
38
|
-
if (
|
|
39
|
+
if (currentId !== requestId) return;
|
|
39
40
|
logs = result.data;
|
|
40
41
|
total = result.total;
|
|
41
42
|
} catch (e) {
|
|
42
|
-
if (
|
|
43
|
+
if (currentId !== requestId) return;
|
|
43
44
|
toast.error((e as Error).message);
|
|
44
45
|
} finally {
|
|
45
|
-
if (
|
|
46
|
+
if (currentId === requestId) loading = false;
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
$effect(() => {
|
|
50
51
|
void page;
|
|
51
|
-
cancelled = true;
|
|
52
|
-
cancelled = false;
|
|
53
52
|
loadLogs();
|
|
54
|
-
return () => { cancelled = true; };
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
let totalPages = $derived(Math.ceil(total / pageSize) || 1);
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
fieldRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; onchange: (v: unknown) => void }]>;
|
|
23
23
|
formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
|
|
24
24
|
headerContent?: Snippet;
|
|
25
|
+
onSuccess?: () => void;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent }: Props = $props();
|
|
28
|
+
let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent, onSuccess }: Props = $props();
|
|
28
29
|
const isReadonly = $derived(mode === 'show');
|
|
29
30
|
|
|
30
31
|
// ─── Resource metadata ────────────────────────────────────────────
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
submitError = null;
|
|
105
106
|
try {
|
|
106
107
|
await form.submit();
|
|
108
|
+
onSuccess?.();
|
|
107
109
|
} catch (e) {
|
|
108
110
|
submitError = e instanceof Error ? e.message : t('common.operationFailed');
|
|
109
111
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
} from '@tanstack/svelte-table';
|
|
12
12
|
import { getCoreRowModel } from '@tanstack/table-core';
|
|
13
13
|
|
|
14
|
-
import { useList, useDelete, getResource, notify } from '@svadmin/core';
|
|
14
|
+
import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
|
|
15
15
|
import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
|
|
16
16
|
import { navigate } from '@svadmin/core/router';
|
|
17
17
|
import { useCan, getAccessControlProvider } from '@svadmin/core';
|
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
const query = listResult;
|
|
171
171
|
const deleteResult = useDelete({ get resource() { return resourceName; } });
|
|
172
172
|
const deleteMutation = deleteResult.mutation;
|
|
173
|
+
const deleteManyResult = useDeleteMany({ get resource() { return resourceName; } });
|
|
174
|
+
const deleteManyMutation = deleteManyResult.mutation;
|
|
173
175
|
|
|
174
176
|
// ─── Permissions ──────────────────────────────────────────────
|
|
175
177
|
const acEnabled = $derived(!!getAccessControlProvider());
|
|
@@ -347,13 +349,11 @@
|
|
|
347
349
|
const ids = Object.keys(rowSelection);
|
|
348
350
|
confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
|
|
349
351
|
confirmAction = async () => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed:
|
|
355
|
-
} else {
|
|
356
|
-
notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: succeeded }) });
|
|
352
|
+
try {
|
|
353
|
+
await deleteManyMutation.mutateAsync({ ids, resource: resourceName, successNotification: false });
|
|
354
|
+
notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: ids.length }) });
|
|
355
|
+
} catch {
|
|
356
|
+
notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
|
|
357
357
|
}
|
|
358
358
|
rowSelection = {};
|
|
359
359
|
confirmOpen = false;
|
|
@@ -374,13 +374,13 @@
|
|
|
374
374
|
return String(val);
|
|
375
375
|
})
|
|
376
376
|
);
|
|
377
|
-
const csv = [headers.join(','), ...rows.map(r => r.map(v => `"${v.replace(
|
|
377
|
+
const csv = [headers.map(h => `"${h.replace(/"/g, '""')}"`).join(','), ...rows.map(r => r.map(v => `"${v.replace(/"/g, '""')}"`).join(','))].join('\n');
|
|
378
378
|
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
|
379
379
|
const link = document.createElement('a');
|
|
380
380
|
link.href = URL.createObjectURL(blob);
|
|
381
381
|
link.download = `${resourceName}_${new Date().toISOString().slice(0, 10)}.csv`;
|
|
382
382
|
link.click();
|
|
383
|
-
URL.revokeObjectURL(link.href);
|
|
383
|
+
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
function goToPage(page: number) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { t } from '@svadmin/core/i18n';
|
|
6
6
|
import * as Breadcrumb from './ui/breadcrumb/index.js';
|
|
7
7
|
|
|
8
|
-
const resources = getResources();
|
|
8
|
+
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
9
9
|
|
|
10
10
|
interface Crumb { label: string; href: string; }
|
|
11
11
|
|
|
@@ -89,26 +89,45 @@
|
|
|
89
89
|
|
|
90
90
|
// ─── Persist on change (debounced) ──────────────────────────
|
|
91
91
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
92
|
+
let lastPersistSnapshot: ChatMessage[] | null = null;
|
|
93
|
+
|
|
94
|
+
function flushPersist(snapshot: ChatMessage[]) {
|
|
95
|
+
if (onPersist) {
|
|
96
|
+
onPersist(snapshot);
|
|
97
|
+
} else if (persistKey) {
|
|
98
|
+
try {
|
|
99
|
+
localStorage.setItem(persistKey, JSON.stringify(snapshot));
|
|
100
|
+
} catch {
|
|
101
|
+
// storage full or unavailable
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
92
105
|
|
|
93
106
|
$effect(() => {
|
|
94
|
-
// Access `messages` to subscribe to changes
|
|
95
107
|
const snapshot = messages;
|
|
96
108
|
if (!initialized) return;
|
|
97
109
|
|
|
98
110
|
if (persistTimer) clearTimeout(persistTimer);
|
|
111
|
+
lastPersistSnapshot = snapshot;
|
|
99
112
|
persistTimer = setTimeout(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} else if (persistKey) {
|
|
103
|
-
try {
|
|
104
|
-
localStorage.setItem(persistKey, JSON.stringify(snapshot));
|
|
105
|
-
} catch {
|
|
106
|
-
// storage full or unavailable
|
|
107
|
-
}
|
|
108
|
-
}
|
|
113
|
+
persistTimer = null;
|
|
114
|
+
flushPersist(snapshot);
|
|
109
115
|
}, 300);
|
|
110
116
|
|
|
111
|
-
return () => {
|
|
117
|
+
return () => {
|
|
118
|
+
if (persistTimer) {
|
|
119
|
+
clearTimeout(persistTimer);
|
|
120
|
+
persistTimer = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
return () => {
|
|
127
|
+
if (lastPersistSnapshot != null) {
|
|
128
|
+
flushPersist(lastPersistSnapshot);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
112
131
|
});
|
|
113
132
|
|
|
114
133
|
function genId(): string {
|
|
@@ -192,6 +211,7 @@
|
|
|
192
211
|
const collectedActions: ChatAction[] = [];
|
|
193
212
|
|
|
194
213
|
for await (const event of stream) {
|
|
214
|
+
if (!isStreaming) break;
|
|
195
215
|
switch (event.type) {
|
|
196
216
|
case 'text':
|
|
197
217
|
assistantMsg.content += event.content;
|
|
@@ -216,7 +236,6 @@
|
|
|
216
236
|
break;
|
|
217
237
|
|
|
218
238
|
case 'approval_request': {
|
|
219
|
-
// Register in the core approval system
|
|
220
239
|
const reqId = event.id;
|
|
221
240
|
pendingApprovalIds = [...pendingApprovalIds, reqId];
|
|
222
241
|
|
|
@@ -227,7 +246,9 @@
|
|
|
227
246
|
content: approved ? `User approved execution of tool '${event.tool}'` : `User rejected execution of tool '${event.tool}'`,
|
|
228
247
|
timestamp: Date.now()
|
|
229
248
|
}];
|
|
230
|
-
|
|
249
|
+
if (agent?.approveToolCall) {
|
|
250
|
+
agent.approveToolCall(reqId, approved);
|
|
251
|
+
}
|
|
231
252
|
});
|
|
232
253
|
|
|
233
254
|
collectedActions.push(
|
|
@@ -269,6 +290,7 @@
|
|
|
269
290
|
|
|
270
291
|
if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
|
|
271
292
|
for await (const chunk of result as AsyncGenerator<string>) {
|
|
293
|
+
if (!isStreaming) break;
|
|
272
294
|
assistantMsg.content += chunk;
|
|
273
295
|
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
274
296
|
scrollToBottom();
|
|
@@ -359,12 +381,22 @@
|
|
|
359
381
|
|
|
360
382
|
/** Simple markdown→html: bold, italic, code blocks, inline code, line breaks */
|
|
361
383
|
function renderMarkdown(text: string): string {
|
|
362
|
-
|
|
363
|
-
|
|
384
|
+
const escaped = escapeHtml(text);
|
|
385
|
+
const codeBlocks: string[] = [];
|
|
386
|
+
let processed = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
387
|
+
const idx = codeBlocks.length;
|
|
388
|
+
codeBlocks.push(`<pre class="chat-code-block"><code>${code}</code></pre>`);
|
|
389
|
+
return `\x00CODEBLOCK${idx}\x00`;
|
|
390
|
+
});
|
|
391
|
+
processed = processed
|
|
364
392
|
.replace(/`([^`]+)`/g, '<code class="chat-inline-code">$1</code>')
|
|
365
393
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
366
394
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
367
395
|
.replace(/\n/g, '<br>');
|
|
396
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
397
|
+
processed = processed.replace(`\x00CODEBLOCK${i}\x00`, codeBlocks[i]);
|
|
398
|
+
}
|
|
399
|
+
return processed;
|
|
368
400
|
}
|
|
369
401
|
|
|
370
402
|
const suggestions = $derived([
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
10
10
|
import { ChevronsUpDown, Check, X, Search } from '@lucide/svelte';
|
|
11
11
|
|
|
12
|
+
function clickOutside(node: HTMLElement) {
|
|
13
|
+
const handler = (e: MouseEvent) => {
|
|
14
|
+
if (!node.contains(e.target as Node)) open = false;
|
|
15
|
+
};
|
|
16
|
+
document.addEventListener('click', handler, true);
|
|
17
|
+
return { destroy() { document.removeEventListener('click', handler, true); } };
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
interface Props {
|
|
13
21
|
resource: string;
|
|
14
22
|
value?: string | number | null;
|
|
@@ -60,6 +68,8 @@
|
|
|
60
68
|
function handleClear(e: Event) {
|
|
61
69
|
e.stopPropagation();
|
|
62
70
|
onchange?.(null);
|
|
71
|
+
open = false;
|
|
72
|
+
searchInputValue = '';
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
function handleSearchInput(e: Event) {
|
|
@@ -69,7 +79,7 @@
|
|
|
69
79
|
}
|
|
70
80
|
</script>
|
|
71
81
|
|
|
72
|
-
<div class="relative">
|
|
82
|
+
<div class="relative" use:clickOutside>
|
|
73
83
|
<Button
|
|
74
84
|
variant="outline"
|
|
75
85
|
type="button"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const resource = $derived(getResource(resourceName));
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function handleSuccess() {
|
|
19
19
|
open = false;
|
|
20
20
|
onSuccess?.();
|
|
21
21
|
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</Sheet.Title>
|
|
30
30
|
</Sheet.Header>
|
|
31
31
|
<div class="flex-1 overflow-y-auto">
|
|
32
|
-
<AutoForm {resourceName} {mode} {id} />
|
|
32
|
+
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
33
33
|
</div>
|
|
34
34
|
</Sheet.Content>
|
|
35
35
|
</Sheet.Root>
|
|
@@ -30,11 +30,18 @@
|
|
|
30
30
|
|
|
31
31
|
// Typed accessors
|
|
32
32
|
const strVal = $derived((value as string) ?? '');
|
|
33
|
-
const numVal = $derived(
|
|
33
|
+
const numVal = $derived(value as number | null | undefined);
|
|
34
34
|
const boolVal = $derived((value as boolean) ?? false);
|
|
35
35
|
const tagsVal = $derived((value as string[]) ?? []);
|
|
36
36
|
const multiVal = $derived((value as (string | number)[]) ?? []);
|
|
37
37
|
const imagesVal = $derived((value as string[]) ?? []);
|
|
38
|
+
let jsonEditText = $state('');
|
|
39
|
+
let jsonEditing = false;
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (jsonEditing) return;
|
|
43
|
+
jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
|
|
44
|
+
});
|
|
38
45
|
|
|
39
46
|
function handleTagKeydown(e: KeyboardEvent) {
|
|
40
47
|
if (e.key === 'Enter') {
|
|
@@ -165,7 +172,7 @@
|
|
|
165
172
|
<Input
|
|
166
173
|
id={field.key}
|
|
167
174
|
type="number"
|
|
168
|
-
value={String(numVal)}
|
|
175
|
+
value={numVal == null ? '' : String(numVal)}
|
|
169
176
|
oninput={(e) => {
|
|
170
177
|
const v = (e.target as HTMLInputElement).value;
|
|
171
178
|
onchange(v === '' ? null : Number(v));
|
|
@@ -359,10 +366,17 @@
|
|
|
359
366
|
{:else if field.type === 'json'}
|
|
360
367
|
<Textarea
|
|
361
368
|
id={field.key}
|
|
362
|
-
value={
|
|
369
|
+
value={jsonEditText}
|
|
370
|
+
onfocus={() => { jsonEditing = true; }}
|
|
371
|
+
onblur={() => {
|
|
372
|
+
jsonEditing = false;
|
|
373
|
+
jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
|
|
374
|
+
}}
|
|
363
375
|
oninput={(e) => {
|
|
376
|
+
const raw = (e.target as HTMLTextAreaElement).value;
|
|
377
|
+
jsonEditText = raw;
|
|
364
378
|
try {
|
|
365
|
-
onchange(JSON.parse(
|
|
379
|
+
onchange(JSON.parse(raw));
|
|
366
380
|
} catch {
|
|
367
381
|
// keep raw text until valid JSON
|
|
368
382
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
let editValue = $state('');
|
|
18
18
|
let inputRef = $state<HTMLInputElement | null>(null);
|
|
19
19
|
let saving = $state(false);
|
|
20
|
+
let savePending = false;
|
|
20
21
|
const displayValue = $derived(String(value ?? ''));
|
|
21
22
|
|
|
22
23
|
const { mutation } = useUpdate({ get resource() { return resourceName; } });
|
|
@@ -31,12 +32,13 @@
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function save() {
|
|
34
|
-
if (!editing || saving) return;
|
|
35
|
-
const newValue = field.type === 'number' ? Number(editValue) : editValue;
|
|
35
|
+
if (!editing || saving || savePending) return;
|
|
36
|
+
const newValue = field.type === 'number' ? (editValue.trim() === '' ? null : (() => { const n = Number(editValue); return isNaN(n) ? value : n; })()) : editValue;
|
|
36
37
|
if (newValue === value) {
|
|
37
38
|
editing = false;
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
savePending = true;
|
|
40
42
|
saving = true;
|
|
41
43
|
try {
|
|
42
44
|
await mutation.mutateAsync({
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
// Keep editing on error
|
|
51
53
|
} finally {
|
|
52
54
|
saving = false;
|
|
55
|
+
savePending = false;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -59,9 +62,16 @@
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function handleKeydown(e: KeyboardEvent) {
|
|
62
|
-
if (e.key === 'Enter')
|
|
65
|
+
if (e.key === 'Enter') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
save();
|
|
68
|
+
}
|
|
63
69
|
if (e.key === 'Escape') cancel();
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
function handleBlur() {
|
|
73
|
+
if (!savePending) save();
|
|
74
|
+
}
|
|
65
75
|
</script>
|
|
66
76
|
|
|
67
77
|
{#if editing}
|
|
@@ -70,7 +80,7 @@
|
|
|
70
80
|
type={field.type === 'number' ? 'number' : 'text'}
|
|
71
81
|
bind:value={editValue}
|
|
72
82
|
onkeydown={handleKeydown}
|
|
73
|
-
onblur={
|
|
83
|
+
onblur={handleBlur}
|
|
74
84
|
disabled={saving}
|
|
75
85
|
class="h-7 w-full rounded border bg-background px-2 text-sm outline-none ring-1 ring-primary/50 focus:ring-2 focus:ring-primary transition-all"
|
|
76
86
|
/>
|
|
@@ -52,9 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
async function handleLogout() {
|
|
54
54
|
if (!auth) return;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
try {
|
|
56
|
+
const result = await auth.logout();
|
|
57
|
+
if (result.success) {
|
|
58
|
+
navigate(result.redirectTo ?? '/login');
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
navigate('/login');
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
|
|
19
19
|
const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
|
|
20
20
|
|
|
21
|
-
function
|
|
21
|
+
function handleSuccess() {
|
|
22
22
|
open = false;
|
|
23
23
|
onSuccess?.();
|
|
24
24
|
}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
|
|
33
33
|
</Dialog.DialogTitle>
|
|
34
34
|
</Dialog.DialogHeader>
|
|
35
|
-
<AutoForm {resourceName} {mode} {id} />
|
|
35
|
+
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
36
36
|
</Dialog.DialogContent>
|
|
37
37
|
</Dialog.Dialog>
|
|
38
38
|
{/if}
|
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
function isActive(itemHref: string | undefined): boolean {
|
|
40
40
|
if (!itemHref) return false;
|
|
41
41
|
if (itemHref === '/') return currentPath === '/';
|
|
42
|
-
|
|
43
|
-
return currentPath.startsWith(itemHref);
|
|
42
|
+
return currentPath.startsWith(itemHref) && (currentPath.length === itemHref.length || currentPath[itemHref.length] === '/');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
/** Check if any child (recursively) is active — used to auto-open parent groups */
|
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
onTimeout: () => void;
|
|
13
13
|
}>();
|
|
14
14
|
|
|
15
|
-
let remaining = $state(
|
|
15
|
+
let remaining = $state(duration);
|
|
16
16
|
let dismissed = $state(false);
|
|
17
17
|
|
|
18
|
-
// Initialize remaining from prop
|
|
19
|
-
$effect.pre(() => {
|
|
20
|
-
remaining = duration;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
18
|
$effect(() => {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const totalMs = duration;
|
|
24
21
|
const interval = setInterval(() => {
|
|
25
|
-
remaining
|
|
22
|
+
remaining = totalMs - (Date.now() - startTime);
|
|
26
23
|
if (remaining <= 0) {
|
|
24
|
+
remaining = 0;
|
|
27
25
|
clearInterval(interval);
|
|
28
26
|
if (!dismissed) {
|
|
29
27
|
dismissed = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ExternalLink } from '@lucide/svelte';
|
|
3
|
+
import { getRouterProvider } from '@svadmin/core';
|
|
3
4
|
|
|
4
5
|
let { value, resourceName, displayValue } = $props<{
|
|
5
6
|
value: string | number | null | undefined;
|
|
@@ -8,7 +9,12 @@
|
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
11
|
const display = $derived(displayValue ?? (value != null ? `#${value}` : '—'));
|
|
11
|
-
const href = $derived(
|
|
12
|
+
const href = $derived.by(() => {
|
|
13
|
+
if (!resourceName || value == null) return undefined;
|
|
14
|
+
const router = getRouterProvider();
|
|
15
|
+
const path = `/${resourceName}/show/${value}`;
|
|
16
|
+
return router?.formatLink?.(path) ?? path;
|
|
17
|
+
});
|
|
12
18
|
</script>
|
|
13
19
|
|
|
14
20
|
{#if href}
|
package/package.json
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.4",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"./src/app.css"
|
|
8
8
|
],
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"src/app.css"
|
|
11
12
|
],
|
|
12
13
|
"main": "dist/index.js",
|
|
13
14
|
"types": "dist/index.d.ts",
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
16
17
|
"types": "./dist/index.d.ts",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
18
|
+
"svelte": "./src/index.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
19
20
|
},
|
|
20
21
|
"./app.css": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"svelte": "./src/app.css",
|
|
23
|
+
"style": "./src/app.css",
|
|
24
|
+
"default": "./dist/app.css"
|
|
24
25
|
},
|
|
25
26
|
"./components/*": "./dist/components/*"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
|
-
"@svadmin/core": "^0.23.
|
|
29
|
+
"@svadmin/core": "^0.23.5",
|
|
29
30
|
"@tanstack/svelte-query": "^6.0.0",
|
|
30
31
|
"highlight.js": "^11.11.1",
|
|
31
32
|
"isomorphic-dompurify": "^3.7.1",
|
|
@@ -48,6 +49,15 @@
|
|
|
48
49
|
},
|
|
49
50
|
"@svadmin/editor": {
|
|
50
51
|
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@svadmin/core": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@tanstack/svelte-query": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"svelte": {
|
|
60
|
+
"optional": true
|
|
51
61
|
}
|
|
52
62
|
},
|
|
53
63
|
"dependencies": {
|
package/src/app.css
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/* Typography — Inter font for clean, modern look */
|
|
2
|
+
|
|
3
|
+
/* @svadmin/ui — Design Tokens (Elygate-inspired)
|
|
4
|
+
* Auto-imported by AdminApp. Can be overridden by consumer's CSS.
|
|
5
|
+
*
|
|
6
|
+
* Based on Elygate dashboard style: Indigo primary, light glassmorphism,
|
|
7
|
+
* professional admin aesthetic with subtle depth.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@layer base {
|
|
11
|
+
/* Global border-color reset — ensures Tailwind border utilities
|
|
12
|
+
* (border-r, border-t, etc.) use the theme color instead of currentColor */
|
|
13
|
+
*,
|
|
14
|
+
::after,
|
|
15
|
+
::before {
|
|
16
|
+
border-color: var(--border);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--radius: 0.75rem;
|
|
21
|
+
--background: oklch(0.975 0.005 264);
|
|
22
|
+
--foreground: oklch(0.13 0.02 265);
|
|
23
|
+
--card: oklch(1 0 0);
|
|
24
|
+
--card-foreground: oklch(0.13 0.02 265);
|
|
25
|
+
--popover: oklch(1 0 0);
|
|
26
|
+
--popover-foreground: oklch(0.13 0.02 265);
|
|
27
|
+
--primary: oklch(0.488 0.243 264.376);
|
|
28
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
29
|
+
--secondary: oklch(0.955 0.008 264);
|
|
30
|
+
--secondary-foreground: oklch(0.2 0.04 265);
|
|
31
|
+
--muted: oklch(0.955 0.005 264);
|
|
32
|
+
--muted-foreground: oklch(0.45 0.03 260);
|
|
33
|
+
--accent: oklch(0.955 0.008 264);
|
|
34
|
+
--accent-foreground: oklch(0.2 0.04 265);
|
|
35
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
36
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
37
|
+
--border: oklch(0.935 0.005 264);
|
|
38
|
+
--input: oklch(0.935 0.005 264);
|
|
39
|
+
--ring: oklch(0.488 0.243 264.376);
|
|
40
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
41
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
42
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
43
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
44
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
45
|
+
--success: oklch(0.6 0.2 145);
|
|
46
|
+
--success-foreground: oklch(0.985 0 0);
|
|
47
|
+
--warning: oklch(0.75 0.18 85);
|
|
48
|
+
--warning-foreground: oklch(0.3 0.08 60);
|
|
49
|
+
--info: oklch(0.6 0.15 250);
|
|
50
|
+
--info-foreground: oklch(0.985 0 0);
|
|
51
|
+
--sidebar: oklch(1 0 0 / 80%);
|
|
52
|
+
--sidebar-foreground: oklch(0.13 0.02 265);
|
|
53
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
54
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
55
|
+
--sidebar-accent: oklch(0.94 0.025 264);
|
|
56
|
+
--sidebar-accent-foreground: oklch(0.33 0.17 264);
|
|
57
|
+
--sidebar-border: oklch(0.935 0.005 264);
|
|
58
|
+
--sidebar-ring: oklch(0.488 0.243 264.376);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dark {
|
|
62
|
+
--background: oklch(0.13 0.015 265);
|
|
63
|
+
--foreground: oklch(0.985 0 0);
|
|
64
|
+
--card: oklch(0.16 0.015 265);
|
|
65
|
+
--card-foreground: oklch(0.985 0 0);
|
|
66
|
+
--popover: oklch(0.16 0.015 265);
|
|
67
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
68
|
+
--primary: oklch(0.585 0.233 264);
|
|
69
|
+
--primary-foreground: oklch(0.15 0.04 265);
|
|
70
|
+
--secondary: oklch(0.23 0.025 265);
|
|
71
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
72
|
+
--muted: oklch(0.23 0.025 265);
|
|
73
|
+
--muted-foreground: oklch(0.7 0.02 260);
|
|
74
|
+
--accent: oklch(0.23 0.025 265);
|
|
75
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
76
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
77
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
78
|
+
--border: oklch(0.27 0.02 265);
|
|
79
|
+
--input: oklch(0.27 0.02 265);
|
|
80
|
+
--ring: oklch(0.55 0.18 264);
|
|
81
|
+
--chart-1: oklch(0.585 0.233 264);
|
|
82
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
83
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
84
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
85
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
86
|
+
--success: oklch(0.65 0.2 150);
|
|
87
|
+
--success-foreground: oklch(0.985 0 0);
|
|
88
|
+
--warning: oklch(0.8 0.15 85);
|
|
89
|
+
--warning-foreground: oklch(0.25 0.05 60);
|
|
90
|
+
--info: oklch(0.65 0.15 250);
|
|
91
|
+
--info-foreground: oklch(0.985 0 0);
|
|
92
|
+
--sidebar: oklch(0.16 0.015 265 / 80%);
|
|
93
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
94
|
+
--sidebar-primary: oklch(0.585 0.233 264);
|
|
95
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
96
|
+
--sidebar-accent: oklch(0.488 0.243 264.376 / 10%);
|
|
97
|
+
--sidebar-accent-foreground: oklch(0.7 0.15 264);
|
|
98
|
+
--sidebar-border: oklch(0.27 0.02 265);
|
|
99
|
+
--sidebar-ring: oklch(0.55 0.18 264);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ── Color Theme Overrides ──────────────────────────────
|
|
103
|
+
* Each [data-theme] selector overrides the primary-related
|
|
104
|
+
* CSS variables for both light and dark mode.
|
|
105
|
+
* Values match builtinPresets in @svadmin/core/theme.
|
|
106
|
+
* This CSS-level fallback prevents FOUC before JS inits.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/* Neutral */
|
|
110
|
+
[data-theme="neutral"] {
|
|
111
|
+
--primary: oklch(0.205 0.006 286);
|
|
112
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
113
|
+
--ring: oklch(0.205 0.006 286);
|
|
114
|
+
--sidebar-primary: oklch(0.205 0.006 286);
|
|
115
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-ring: oklch(0.205 0.006 286);
|
|
117
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
118
|
+
}
|
|
119
|
+
.dark[data-theme="neutral"],
|
|
120
|
+
.dark [data-theme="neutral"] {
|
|
121
|
+
--primary: oklch(0.922 0.004 286);
|
|
122
|
+
--primary-foreground: oklch(0.205 0.006 286);
|
|
123
|
+
--ring: oklch(0.556 0.004 286);
|
|
124
|
+
--sidebar-primary: oklch(0.922 0.004 286);
|
|
125
|
+
--sidebar-primary-foreground: oklch(0.205 0.006 286);
|
|
126
|
+
--sidebar-ring: oklch(0.556 0.004 286);
|
|
127
|
+
--chart-1: oklch(0.585 0.233 264);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Indigo (default) */
|
|
131
|
+
[data-theme="indigo"] {
|
|
132
|
+
--primary: oklch(0.488 0.243 264.376);
|
|
133
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
134
|
+
--ring: oklch(0.488 0.243 264.376);
|
|
135
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
136
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
137
|
+
--sidebar-ring: oklch(0.488 0.243 264.376);
|
|
138
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
139
|
+
}
|
|
140
|
+
.dark[data-theme="indigo"],
|
|
141
|
+
.dark [data-theme="indigo"] {
|
|
142
|
+
--primary: oklch(0.585 0.233 264);
|
|
143
|
+
--primary-foreground: oklch(0.15 0.04 265);
|
|
144
|
+
--ring: oklch(0.55 0.18 264);
|
|
145
|
+
--sidebar-primary: oklch(0.585 0.233 264);
|
|
146
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
147
|
+
--sidebar-ring: oklch(0.55 0.18 264);
|
|
148
|
+
--chart-1: oklch(0.585 0.233 264);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Blue */
|
|
152
|
+
[data-theme="blue"] {
|
|
153
|
+
--primary: oklch(0.546 0.245 262.881);
|
|
154
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
155
|
+
--ring: oklch(0.546 0.245 262.881);
|
|
156
|
+
--sidebar-primary: oklch(0.546 0.245 262.881);
|
|
157
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
158
|
+
--sidebar-ring: oklch(0.546 0.245 262.881);
|
|
159
|
+
--chart-1: oklch(0.546 0.245 262.881);
|
|
160
|
+
}
|
|
161
|
+
.dark[data-theme="blue"],
|
|
162
|
+
.dark [data-theme="blue"] {
|
|
163
|
+
--primary: oklch(0.623 0.214 259.815);
|
|
164
|
+
--primary-foreground: oklch(0.15 0.04 265);
|
|
165
|
+
--ring: oklch(0.546 0.245 262.881);
|
|
166
|
+
--sidebar-primary: oklch(0.623 0.214 259.815);
|
|
167
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
168
|
+
--sidebar-ring: oklch(0.546 0.245 262.881);
|
|
169
|
+
--chart-1: oklch(0.623 0.214 259.815);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Green */
|
|
173
|
+
[data-theme="green"] {
|
|
174
|
+
--primary: oklch(0.527 0.185 150.069);
|
|
175
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
176
|
+
--ring: oklch(0.527 0.185 150.069);
|
|
177
|
+
--sidebar-primary: oklch(0.527 0.185 150.069);
|
|
178
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
179
|
+
--sidebar-ring: oklch(0.527 0.185 150.069);
|
|
180
|
+
--chart-1: oklch(0.527 0.185 150.069);
|
|
181
|
+
}
|
|
182
|
+
.dark[data-theme="green"],
|
|
183
|
+
.dark [data-theme="green"] {
|
|
184
|
+
--primary: oklch(0.627 0.194 149.214);
|
|
185
|
+
--primary-foreground: oklch(0.15 0.05 150);
|
|
186
|
+
--ring: oklch(0.527 0.185 150.069);
|
|
187
|
+
--sidebar-primary: oklch(0.627 0.194 149.214);
|
|
188
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
189
|
+
--sidebar-ring: oklch(0.527 0.185 150.069);
|
|
190
|
+
--chart-1: oklch(0.627 0.194 149.214);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Rose */
|
|
194
|
+
[data-theme="rose"] {
|
|
195
|
+
--primary: oklch(0.577 0.245 27.325);
|
|
196
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
197
|
+
--ring: oklch(0.577 0.245 27.325);
|
|
198
|
+
--sidebar-primary: oklch(0.577 0.245 27.325);
|
|
199
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
200
|
+
--sidebar-ring: oklch(0.577 0.245 27.325);
|
|
201
|
+
--chart-1: oklch(0.577 0.245 27.325);
|
|
202
|
+
}
|
|
203
|
+
.dark[data-theme="rose"],
|
|
204
|
+
.dark [data-theme="rose"] {
|
|
205
|
+
--primary: oklch(0.577 0.245 27.325);
|
|
206
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
207
|
+
--ring: oklch(0.577 0.245 27.325);
|
|
208
|
+
--sidebar-primary: oklch(0.577 0.245 27.325);
|
|
209
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
210
|
+
--sidebar-ring: oklch(0.577 0.245 27.325);
|
|
211
|
+
--chart-1: oklch(0.577 0.245 27.325);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Orange */
|
|
215
|
+
[data-theme="orange"] {
|
|
216
|
+
--primary: oklch(0.646 0.222 41.116);
|
|
217
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
218
|
+
--ring: oklch(0.646 0.222 41.116);
|
|
219
|
+
--sidebar-primary: oklch(0.646 0.222 41.116);
|
|
220
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
221
|
+
--sidebar-ring: oklch(0.646 0.222 41.116);
|
|
222
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
223
|
+
}
|
|
224
|
+
.dark[data-theme="orange"],
|
|
225
|
+
.dark [data-theme="orange"] {
|
|
226
|
+
--primary: oklch(0.646 0.222 41.116);
|
|
227
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
228
|
+
--ring: oklch(0.646 0.222 41.116);
|
|
229
|
+
--sidebar-primary: oklch(0.646 0.222 41.116);
|
|
230
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
231
|
+
--sidebar-ring: oklch(0.646 0.222 41.116);
|
|
232
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Violet */
|
|
236
|
+
[data-theme="violet"] {
|
|
237
|
+
--primary: oklch(0.541 0.281 293.009);
|
|
238
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
239
|
+
--ring: oklch(0.541 0.281 293.009);
|
|
240
|
+
--sidebar-primary: oklch(0.541 0.281 293.009);
|
|
241
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
242
|
+
--sidebar-ring: oklch(0.541 0.281 293.009);
|
|
243
|
+
--chart-1: oklch(0.541 0.281 293.009);
|
|
244
|
+
}
|
|
245
|
+
.dark[data-theme="violet"],
|
|
246
|
+
.dark [data-theme="violet"] {
|
|
247
|
+
--primary: oklch(0.541 0.281 293.009);
|
|
248
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
249
|
+
--ring: oklch(0.541 0.281 293.009);
|
|
250
|
+
--sidebar-primary: oklch(0.541 0.281 293.009);
|
|
251
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
252
|
+
--sidebar-ring: oklch(0.541 0.281 293.009);
|
|
253
|
+
--chart-1: oklch(0.541 0.281 293.009);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Stripe-inspired Soft Shadows — premium, multi-layered depth */
|
|
257
|
+
[data-slot="button"],
|
|
258
|
+
[data-slot="input"],
|
|
259
|
+
[data-slot="select-trigger"],
|
|
260
|
+
[data-slot="select"],
|
|
261
|
+
[data-slot="textarea"] {
|
|
262
|
+
/* Subtle interaction shadow */
|
|
263
|
+
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Stripe-style: form inputs use card background instead of transparent */
|
|
267
|
+
[data-slot="input"],
|
|
268
|
+
[data-slot="textarea"],
|
|
269
|
+
[data-slot="select"] {
|
|
270
|
+
background-color: var(--card);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
[data-slot="card"] {
|
|
274
|
+
/* Classic Stripe soft shadow: faint ambient + directional drop */
|
|
275
|
+
box-shadow: 0 2px 5px -1px rgb(0 0 0 / 0.04), 0 1px 1px -1px rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
[data-slot="dialog-content"],
|
|
279
|
+
[data-slot="sheet-content"] {
|
|
280
|
+
/* Deep floating layer for modals */
|
|
281
|
+
box-shadow: 0 10px 30px -5px rgb(0 0 0 / 0.08), 0 20px 40px -10px rgb(0 0 0 / 0.04);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
body {
|
|
286
|
+
font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
287
|
+
-webkit-font-smoothing: antialiased;
|
|
288
|
+
-moz-osx-font-smoothing: grayscale;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Premium Typography Refinements */
|
|
292
|
+
h1, h2, h3 {
|
|
293
|
+
letter-spacing: -0.025em;
|
|
294
|
+
font-weight: 600;
|
|
295
|
+
}
|
|
296
|
+
h4, h5, h6 {
|
|
297
|
+
letter-spacing: -0.01em;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Micro-interactions: Linear/Apple ecosystem click scales */
|
|
301
|
+
[data-slot="button"],
|
|
302
|
+
button {
|
|
303
|
+
transition: transform 0.1s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.15s ease, box-shadow 0.15s ease;
|
|
304
|
+
}
|
|
305
|
+
[data-slot="button"]:active:not(:disabled),
|
|
306
|
+
button:active:not(:disabled) {
|
|
307
|
+
transform: scale(0.98);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Subtle glowing focus ring for inputs */
|
|
311
|
+
[data-slot="button"]:focus-visible,
|
|
312
|
+
[data-slot="input"]:focus-visible,
|
|
313
|
+
[data-slot="textarea"]:focus-visible,
|
|
314
|
+
[data-slot="select-trigger"]:focus-visible {
|
|
315
|
+
box-shadow: 0 0 0 1px var(--color-background), 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent) !important;
|
|
316
|
+
border-color: var(--color-primary);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Base scrollbar styling (Firefox + Standard) */
|
|
320
|
+
* {
|
|
321
|
+
scrollbar-width: thin;
|
|
322
|
+
scrollbar-color: oklch(0.8 0 0) transparent;
|
|
323
|
+
}
|
|
324
|
+
.dark * {
|
|
325
|
+
scrollbar-color: oklch(0.35 0 0) transparent;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Scrollbar styling (Webkit) */
|
|
329
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
330
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
331
|
+
::-webkit-scrollbar-thumb { background: oklch(0.8 0 0); border-radius: 3px; }
|
|
332
|
+
::-webkit-scrollbar-thumb:hover { background: oklch(0.65 0 0); }
|
|
333
|
+
.dark ::-webkit-scrollbar-thumb { background: oklch(0.35 0 0); }
|
|
334
|
+
.dark ::-webkit-scrollbar-thumb:hover { background: oklch(0.45 0 0); }
|
|
335
|
+
|
|
336
|
+
/* ① Card Hover Elevation Lift — Stripe-style float on hover */
|
|
337
|
+
[data-slot="card"] {
|
|
338
|
+
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
|
339
|
+
}
|
|
340
|
+
@media (hover: hover) {
|
|
341
|
+
[data-slot="card"]:hover {
|
|
342
|
+
box-shadow: 0 6px 16px -4px rgb(0 0 0 / 0.06), 0 4px 8px -2px rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
|
|
343
|
+
transform: translateY(-1px);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ② Table Row Hover — primary color tint instead of plain gray */
|
|
348
|
+
[data-slot="table-row"]:hover {
|
|
349
|
+
background-color: color-mix(in oklch, var(--primary) 4%, var(--background)) !important;
|
|
350
|
+
}
|
|
351
|
+
[data-slot="table-row"] {
|
|
352
|
+
transition: background-color 0.15s ease;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ③ Sidebar Active Indicator — Linear-style left accent bar */
|
|
356
|
+
.sidebar-nav-active {
|
|
357
|
+
position: relative;
|
|
358
|
+
}
|
|
359
|
+
.sidebar-nav-active::before {
|
|
360
|
+
content: '';
|
|
361
|
+
position: absolute;
|
|
362
|
+
left: 0;
|
|
363
|
+
top: 50%;
|
|
364
|
+
transform: translateY(-50%);
|
|
365
|
+
width: 3px;
|
|
366
|
+
height: 60%;
|
|
367
|
+
border-radius: 0 3px 3px 0;
|
|
368
|
+
background: var(--primary);
|
|
369
|
+
opacity: 1;
|
|
370
|
+
transition: opacity 0.2s ease;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* ④ Skeleton Shimmer — metallic light sweep instead of static pulse */
|
|
374
|
+
@keyframes svadmin-shimmer {
|
|
375
|
+
0% { background-position: -200% 0; }
|
|
376
|
+
100% { background-position: 200% 0; }
|
|
377
|
+
}
|
|
378
|
+
[data-slot="skeleton"] {
|
|
379
|
+
animation: svadmin-shimmer 1.5s ease-in-out infinite !important;
|
|
380
|
+
background: linear-gradient(90deg, var(--muted) 25%, color-mix(in oklch, var(--muted) 60%, white) 50%, var(--muted) 75%) !important;
|
|
381
|
+
background-size: 200% 100% !important;
|
|
382
|
+
}
|
|
383
|
+
.dark [data-slot="skeleton"] {
|
|
384
|
+
background: linear-gradient(90deg, var(--muted) 25%, color-mix(in oklch, var(--muted) 60%, var(--foreground)) 50%, var(--muted) 75%) !important;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* ⑤ Route Page Transition — fade + subtle slide-up */
|
|
388
|
+
@keyframes svadmin-page-enter {
|
|
389
|
+
from {
|
|
390
|
+
opacity: 0;
|
|
391
|
+
transform: translateY(6px);
|
|
392
|
+
}
|
|
393
|
+
to {
|
|
394
|
+
opacity: 1;
|
|
395
|
+
transform: translateY(0);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
.svadmin-page-enter {
|
|
399
|
+
animation: svadmin-page-enter 0.25s ease-out;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ⑥ Toast slide-in refinement (sonner-svelte) — tighter timing */
|
|
403
|
+
[data-sonner-toaster] [data-sonner-toast] {
|
|
404
|
+
transition-duration: 200ms !important;
|
|
405
|
+
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* ⑦ Gradient Top Border on Cards — Vercel dashboard style */
|
|
409
|
+
[data-slot="card"]::before {
|
|
410
|
+
content: '';
|
|
411
|
+
position: absolute;
|
|
412
|
+
top: 0;
|
|
413
|
+
left: 0;
|
|
414
|
+
right: 0;
|
|
415
|
+
height: 1px;
|
|
416
|
+
background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--primary) 20%, transparent), transparent);
|
|
417
|
+
border-radius: inherit;
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
}
|
|
420
|
+
[data-slot="card"] {
|
|
421
|
+
position: relative;
|
|
422
|
+
overflow: hidden;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ⑧ Dark Mode Primary Glow — Linear-style neon accents */
|
|
426
|
+
.dark [data-slot="button"][data-variant="default"],
|
|
427
|
+
.dark [data-slot="badge"][data-variant="default"] {
|
|
428
|
+
box-shadow: 0 0 12px -3px color-mix(in oklch, var(--primary) 40%, transparent);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ⑨ Form Validation Shake Animation */
|
|
432
|
+
@keyframes svadmin-shake {
|
|
433
|
+
0%, 100% { transform: translateX(0); }
|
|
434
|
+
20% { transform: translateX(-6px); }
|
|
435
|
+
40% { transform: translateX(5px); }
|
|
436
|
+
60% { transform: translateX(-4px); }
|
|
437
|
+
80% { transform: translateX(2px); }
|
|
438
|
+
}
|
|
439
|
+
.svadmin-shake {
|
|
440
|
+
animation: svadmin-shake 0.4s ease-out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ⑩ Table Row Staggered Entrance */
|
|
444
|
+
@keyframes svadmin-row-enter {
|
|
445
|
+
from {
|
|
446
|
+
opacity: 0;
|
|
447
|
+
transform: translateY(4px);
|
|
448
|
+
}
|
|
449
|
+
to {
|
|
450
|
+
opacity: 1;
|
|
451
|
+
transform: translateY(0);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
[data-slot="table-body"] [data-slot="table-row"] {
|
|
455
|
+
animation: svadmin-row-enter 0.3s ease-out both;
|
|
456
|
+
}
|
|
457
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(1) { animation-delay: 0s; }
|
|
458
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(2) { animation-delay: 0.03s; }
|
|
459
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(3) { animation-delay: 0.06s; }
|
|
460
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(4) { animation-delay: 0.09s; }
|
|
461
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(5) { animation-delay: 0.12s; }
|
|
462
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(6) { animation-delay: 0.15s; }
|
|
463
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(7) { animation-delay: 0.18s; }
|
|
464
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(8) { animation-delay: 0.21s; }
|
|
465
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(9) { animation-delay: 0.24s; }
|
|
466
|
+
[data-slot="table-body"] [data-slot="table-row"]:nth-child(10) { animation-delay: 0.27s; }
|
|
467
|
+
|
|
468
|
+
/* ⑪ Accessibility: Respect reduced motion preferences */
|
|
469
|
+
@media (prefers-reduced-motion: reduce) {
|
|
470
|
+
*, *::before, *::after {
|
|
471
|
+
animation-duration: 0.01ms !important;
|
|
472
|
+
animation-iteration-count: 1 !important;
|
|
473
|
+
transition-duration: 0.01ms !important;
|
|
474
|
+
scroll-behavior: auto !important;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* ⑫b Safe Area Inset — iPhone notch / Home Bar support */
|
|
479
|
+
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
|
480
|
+
main {
|
|
481
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* ⑫ Optimistic Delete — row fade-out visual feedback */
|
|
486
|
+
@keyframes svadmin-row-delete {
|
|
487
|
+
from {
|
|
488
|
+
opacity: 1;
|
|
489
|
+
transform: translateX(0);
|
|
490
|
+
}
|
|
491
|
+
to {
|
|
492
|
+
opacity: 0;
|
|
493
|
+
transform: translateX(20px);
|
|
494
|
+
height: 0;
|
|
495
|
+
padding: 0;
|
|
496
|
+
margin: 0;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
.svadmin-deleting {
|
|
500
|
+
animation: svadmin-row-delete 0.3s ease-out forwards;
|
|
501
|
+
pointer-events: none;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* ⑬ Popover & Dropdown — frosted glass backdrop + spring entrance */
|
|
505
|
+
[data-slot="popover-content"],
|
|
506
|
+
[data-slot="dropdown-menu-content"],
|
|
507
|
+
[data-slot="context-menu-content"] {
|
|
508
|
+
backdrop-filter: blur(8px);
|
|
509
|
+
-webkit-backdrop-filter: blur(8px);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ⑭ Tooltip spring entrance */
|
|
513
|
+
[data-slot="tooltip-content"] {
|
|
514
|
+
animation: svadmin-tooltip-enter 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
515
|
+
}
|
|
516
|
+
@keyframes svadmin-tooltip-enter {
|
|
517
|
+
from {
|
|
518
|
+
opacity: 0;
|
|
519
|
+
transform: scale(0.96);
|
|
520
|
+
}
|
|
521
|
+
to {
|
|
522
|
+
opacity: 1;
|
|
523
|
+
transform: scale(1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* ⑮ Smooth scrolling for content areas */
|
|
528
|
+
main {
|
|
529
|
+
scroll-behavior: smooth;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* ⑯ RTL (Right-to-Left) Language Support */
|
|
533
|
+
[dir="rtl"] {
|
|
534
|
+
direction: rtl;
|
|
535
|
+
}
|
|
536
|
+
[dir="rtl"] aside {
|
|
537
|
+
left: auto;
|
|
538
|
+
right: 0;
|
|
539
|
+
}
|
|
540
|
+
[dir="rtl"] .sidebar-nav-active::before {
|
|
541
|
+
left: auto;
|
|
542
|
+
right: 0;
|
|
543
|
+
border-radius: 3px 0 0 3px;
|
|
544
|
+
}
|
|
545
|
+
/* Mirror sidebar margins for RTL */
|
|
546
|
+
[dir="rtl"] .md\:ml-64 {
|
|
547
|
+
margin-left: 0;
|
|
548
|
+
margin-right: 16rem;
|
|
549
|
+
}
|
|
550
|
+
[dir="rtl"] .md\:ml-16 {
|
|
551
|
+
margin-left: 0;
|
|
552
|
+
margin-right: 4rem;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* ⑰ Accessibility Minimum Touch Target — Apple HIG 44px minimum */
|
|
556
|
+
@media (max-width: 640px) and (pointer: coarse) {
|
|
557
|
+
[data-slot="button"] {
|
|
558
|
+
position: relative;
|
|
559
|
+
}
|
|
560
|
+
[data-slot="button"]::after {
|
|
561
|
+
content: '';
|
|
562
|
+
position: absolute;
|
|
563
|
+
top: 50%;
|
|
564
|
+
left: 50%;
|
|
565
|
+
transform: translate(-50%, -50%);
|
|
566
|
+
min-width: 44px;
|
|
567
|
+
min-height: 44px;
|
|
568
|
+
width: 100%;
|
|
569
|
+
height: 100%;
|
|
570
|
+
z-index: 1;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Tailwind CSS v4 — Map design tokens to utility classes */
|
|
575
|
+
@theme {
|
|
576
|
+
--color-background: var(--background);
|
|
577
|
+
--color-foreground: var(--foreground);
|
|
578
|
+
--color-card: var(--card);
|
|
579
|
+
--color-card-foreground: var(--card-foreground);
|
|
580
|
+
--color-popover: var(--popover);
|
|
581
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
582
|
+
--color-primary: var(--primary);
|
|
583
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
584
|
+
--color-secondary: var(--secondary);
|
|
585
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
586
|
+
--color-muted: var(--muted);
|
|
587
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
588
|
+
--color-accent: var(--accent);
|
|
589
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
590
|
+
--color-destructive: var(--destructive);
|
|
591
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
592
|
+
--color-border: var(--border);
|
|
593
|
+
--color-input: var(--input);
|
|
594
|
+
--color-ring: var(--ring);
|
|
595
|
+
--color-chart-1: var(--chart-1);
|
|
596
|
+
--color-chart-2: var(--chart-2);
|
|
597
|
+
--color-chart-3: var(--chart-3);
|
|
598
|
+
--color-chart-4: var(--chart-4);
|
|
599
|
+
--color-chart-5: var(--chart-5);
|
|
600
|
+
--color-sidebar: var(--sidebar);
|
|
601
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
602
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
603
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
604
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
605
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
606
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
607
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
608
|
+
--color-success: var(--success);
|
|
609
|
+
--color-success-foreground: var(--success-foreground);
|
|
610
|
+
--color-warning: var(--warning);
|
|
611
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
612
|
+
--color-info: var(--info);
|
|
613
|
+
--color-info-foreground: var(--info-foreground);
|
|
614
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
615
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
616
|
+
--radius-lg: var(--radius);
|
|
617
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* ─── svadmin UI Components CSS ─────────────────────────────── */
|
|
621
|
+
/* These rules were moved from .svelte files to eliminate <style> tags */
|
|
622
|
+
|
|
623
|
+
/* MarkdownRenderer Styles */
|
|
624
|
+
.prose {
|
|
625
|
+
line-height: 1.6;
|
|
626
|
+
}
|
|
627
|
+
.prose p {
|
|
628
|
+
margin-top: 0.5em;
|
|
629
|
+
margin-bottom: 0.5em;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* Streaming cursor effect */
|
|
633
|
+
.streaming > *:last-child::after {
|
|
634
|
+
content: '▋';
|
|
635
|
+
display: inline-block;
|
|
636
|
+
animation: svadmin-blink 1s step-start infinite;
|
|
637
|
+
margin-left: 2px;
|
|
638
|
+
vertical-align: baseline;
|
|
639
|
+
color: currentColor;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@keyframes svadmin-blink {
|
|
643
|
+
50% { opacity: 0; }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* Handle copied state for dynamically injected buttons */
|
|
647
|
+
.copy-btn[data-copied-id] {
|
|
648
|
+
color: #10b981; /* Tailwind emerald-500 */
|
|
649
|
+
}
|
|
650
|
+
.copy-btn[data-copied-id] svg {
|
|
651
|
+
/* Swap to Check icon */
|
|
652
|
+
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="%2310b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* ChatDialog Styles */
|
|
656
|
+
.chat-markdown pre.chat-code-block {
|
|
657
|
+
background: var(--color-muted, hsl(var(--muted)));
|
|
658
|
+
border-radius: 0.5rem;
|
|
659
|
+
padding: 0.75rem;
|
|
660
|
+
margin: 0.5rem 0;
|
|
661
|
+
overflow-x: auto;
|
|
662
|
+
font-size: 0.75rem;
|
|
663
|
+
line-height: 1.5;
|
|
664
|
+
}
|
|
665
|
+
.chat-markdown code.chat-inline-code {
|
|
666
|
+
background: var(--color-muted, hsl(var(--muted)));
|
|
667
|
+
padding: 0.125rem 0.375rem;
|
|
668
|
+
border-radius: 0.25rem;
|
|
669
|
+
font-size: 0.8125em;
|
|
670
|
+
font-family: ui-monospace, monospace;
|
|
671
|
+
}
|
|
672
|
+
.chat-markdown strong {
|
|
673
|
+
font-weight: 600;
|
|
674
|
+
}
|