@svadmin/ui 0.31.1 → 0.32.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/dist/app.css +7 -6
- package/dist/components/AdminApp.svelte +18 -23
- package/dist/components/AdminApp.svelte.d.ts +3 -1
- package/dist/components/AuditLogViewer.svelte +9 -2
- package/dist/components/AutoForm.svelte +29 -8
- package/dist/components/AutoForm.svelte.d.ts +1 -1
- package/dist/components/AutoTable.svelte +33 -13
- package/dist/components/CommandPalette.svelte +1 -1
- package/dist/components/ConfirmDialog.svelte +16 -2
- package/dist/components/FieldRenderer.svelte +37 -10
- package/dist/components/FieldRenderer.svelte.d.ts +1 -1
- package/dist/components/Header.svelte +5 -6
- package/dist/components/Layout.svelte +0 -2
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/NavigateToResource.svelte +4 -2
- package/dist/components/PermissionMatrix.svelte +6 -1
- package/dist/components/ShowPage.svelte +9 -0
- package/dist/components/Sidebar.svelte +16 -9
- package/dist/components/SidebarItem.svelte +2 -2
- package/dist/components/TenantSwitcher.svelte +2 -2
- package/dist/components/VoiceInput.svelte +12 -0
- package/dist/components/ui/dialog/dialog-content.svelte +1 -1
- package/dist/components/ui/dialog/dialog-overlay.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -1
- package/package.json +2 -2
package/dist/app.css
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
:root {
|
|
20
|
-
--radius: 0.
|
|
21
|
-
--background: oklch(0.
|
|
20
|
+
--radius: 0.75rem;
|
|
21
|
+
--background: oklch(0.975 0.005 264);
|
|
22
22
|
--foreground: oklch(0.13 0.02 265);
|
|
23
23
|
--card: oklch(1 0 0);
|
|
24
24
|
--card-foreground: oklch(0.13 0.02 265);
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
|
|
273
273
|
[data-slot="card"] {
|
|
274
274
|
/* Classic Stripe soft shadow: faint ambient + directional drop */
|
|
275
|
-
box-shadow: 0 2px
|
|
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
276
|
}
|
|
277
277
|
|
|
278
278
|
[data-slot="dialog-content"],
|
|
@@ -304,14 +304,15 @@ button {
|
|
|
304
304
|
}
|
|
305
305
|
[data-slot="button"]:active:not(:disabled),
|
|
306
306
|
button:active:not(:disabled) {
|
|
307
|
-
transform: scale(0.
|
|
307
|
+
transform: scale(0.98);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/* Subtle glowing focus ring for inputs */
|
|
311
|
+
[data-slot="button"]:focus-visible,
|
|
311
312
|
[data-slot="input"]:focus-visible,
|
|
312
313
|
[data-slot="textarea"]:focus-visible,
|
|
313
314
|
[data-slot="select-trigger"]:focus-visible {
|
|
314
|
-
box-shadow: 0 0 0 1px var(--color-background), 0 0 0 3px color-mix(in oklch, var(--color-primary)
|
|
315
|
+
box-shadow: 0 0 0 1px var(--color-background), 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent) !important;
|
|
315
316
|
border-color: var(--color-primary);
|
|
316
317
|
}
|
|
317
318
|
|
|
@@ -338,7 +339,7 @@ button:active:not(:disabled) {
|
|
|
338
339
|
}
|
|
339
340
|
@media (hover: hover) {
|
|
340
341
|
[data-slot="card"]:hover {
|
|
341
|
-
box-shadow: 0
|
|
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);
|
|
342
343
|
transform: translateY(-1px);
|
|
343
344
|
}
|
|
344
345
|
}
|
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
import AutoTable from './AutoTable.svelte';
|
|
14
14
|
import AutoForm from './AutoForm.svelte';
|
|
15
15
|
import ShowPage from './ShowPage.svelte';
|
|
16
|
+
|
|
17
|
+
// Module-level QueryClient — survives component remounts (SPA only)
|
|
18
|
+
let _queryClient: QueryClient | null = null;
|
|
19
|
+
function getOrCreateQueryClient(): QueryClient {
|
|
20
|
+
if (!_queryClient) {
|
|
21
|
+
_queryClient = new QueryClient({
|
|
22
|
+
defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return _queryClient;
|
|
26
|
+
}
|
|
16
27
|
import Toast from './Toast.svelte';
|
|
17
28
|
import LoginPage from './LoginPage.svelte';
|
|
18
29
|
import RegisterPage from './RegisterPage.svelte';
|
|
@@ -40,7 +51,7 @@
|
|
|
40
51
|
/** Theme configuration for dark-first mode, CSS overrides, etc. */
|
|
41
52
|
themeConfig?: ThemeConfig;
|
|
42
53
|
dashboard?: Snippet;
|
|
43
|
-
loginPage?: Snippet
|
|
54
|
+
loginPage?: Snippet<[{ onSuccess: () => void }]>;
|
|
44
55
|
/** Override default components via DI */
|
|
45
56
|
components?: Partial<ComponentRegistry>;
|
|
46
57
|
/** Custom multi-level menu configuration */
|
|
@@ -78,25 +89,13 @@
|
|
|
78
89
|
Skeleton: Skeleton as unknown as ComponentRegistry['Skeleton'],
|
|
79
90
|
};
|
|
80
91
|
|
|
81
|
-
|
|
82
|
-
$effect(() => {
|
|
83
|
-
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
84
|
-
});
|
|
92
|
+
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
85
93
|
|
|
86
94
|
// Resolve router provider (default to hash)
|
|
87
95
|
const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
|
|
88
96
|
const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
|
|
89
97
|
|
|
90
|
-
// Set up
|
|
91
|
-
setDataProvider(dataProvider);
|
|
92
|
-
setAuthProvider(authProvider);
|
|
93
|
-
setResources(resources);
|
|
94
|
-
setRouterProvider(resolvedRouter);
|
|
95
|
-
if (locale) setLocale(locale);
|
|
96
|
-
if (userThemeConfig) configureTheme(userThemeConfig);
|
|
97
|
-
if (defaultTheme) setTheme(defaultTheme);
|
|
98
|
-
|
|
99
|
-
// Set up context — use $effect so prop changes are tracked
|
|
98
|
+
// Set up context — use $effect.pre so prop changes are tracked and children can access during first render
|
|
100
99
|
$effect.pre(() => {
|
|
101
100
|
setDataProvider(dataProvider);
|
|
102
101
|
setAuthProvider(authProvider);
|
|
@@ -107,14 +106,10 @@
|
|
|
107
106
|
if (defaultTheme) setTheme(defaultTheme);
|
|
108
107
|
});
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
queries: { staleTime: 30_000, retry: 1 },
|
|
113
|
-
},
|
|
114
|
-
});
|
|
109
|
+
// QueryClient — created once and reused across remounts
|
|
110
|
+
const queryClient = getOrCreateQueryClient();
|
|
115
111
|
|
|
116
|
-
// Initialize router with provider
|
|
117
|
-
initRouter(resolvedRouter);
|
|
112
|
+
// Initialize router with provider
|
|
118
113
|
$effect.pre(() => {
|
|
119
114
|
initRouter(resolvedRouter);
|
|
120
115
|
});
|
|
@@ -169,7 +164,7 @@
|
|
|
169
164
|
</div>
|
|
170
165
|
</div>
|
|
171
166
|
{:else if route === '/login' && loginPage}
|
|
172
|
-
{@render loginPage()}
|
|
167
|
+
{@render loginPage({ onSuccess: () => { isAuthenticated = true; navigate('/'); } })}
|
|
173
168
|
{:else if route === '/login' && authProvider}
|
|
174
169
|
<LoginPage {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
|
|
175
170
|
{:else if route === '/register' && authProvider?.register}
|
|
@@ -13,7 +13,9 @@ interface Props {
|
|
|
13
13
|
/** Theme configuration for dark-first mode, CSS overrides, etc. */
|
|
14
14
|
themeConfig?: ThemeConfig;
|
|
15
15
|
dashboard?: Snippet;
|
|
16
|
-
loginPage?: Snippet
|
|
16
|
+
loginPage?: Snippet<[{
|
|
17
|
+
onSuccess: () => void;
|
|
18
|
+
}]>;
|
|
17
19
|
/** Override default components via DI */
|
|
18
20
|
components?: Partial<ComponentRegistry>;
|
|
19
21
|
/** Custom multi-level menu configuration */
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
let drawerOpen = $state(false);
|
|
24
24
|
let drawerLog = $state<AuditLog | null>(null);
|
|
25
25
|
|
|
26
|
+
let cancelled = $state(false);
|
|
27
|
+
|
|
26
28
|
async function loadLogs() {
|
|
27
29
|
if (!authProvider?.getAuditLogs) {
|
|
28
30
|
error = t('settings.auditNotSupported') ?? 'Audit logs are not supported by the current AuthProvider';
|
|
@@ -30,21 +32,26 @@
|
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
loading = true;
|
|
35
|
+
error = null;
|
|
33
36
|
try {
|
|
34
37
|
const result = await authProvider.getAuditLogs({ page, pageSize });
|
|
38
|
+
if (cancelled) return;
|
|
35
39
|
logs = result.data;
|
|
36
40
|
total = result.total;
|
|
37
41
|
} catch (e) {
|
|
42
|
+
if (cancelled) return;
|
|
38
43
|
toast.error((e as Error).message);
|
|
39
44
|
} finally {
|
|
40
|
-
loading = false;
|
|
45
|
+
if (!cancelled) loading = false;
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
$effect(() => {
|
|
45
|
-
// Track `page` to re-fetch on pagination — this is the single trigger
|
|
46
50
|
void page;
|
|
51
|
+
cancelled = true;
|
|
52
|
+
cancelled = false;
|
|
47
53
|
loadLogs();
|
|
54
|
+
return () => { cancelled = true; };
|
|
48
55
|
});
|
|
49
56
|
|
|
50
57
|
let totalPages = $derived(Math.ceil(total / pageSize) || 1);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useForm, getResource, deriveValidator } from '@svadmin/core';
|
|
3
|
+
import { slide } from 'svelte/transition';
|
|
3
4
|
import type { FieldDefinition } from '@svadmin/core';
|
|
4
5
|
import { t } from '@svadmin/core/i18n';
|
|
5
6
|
import { navigate } from '@svadmin/core/router';
|
|
@@ -17,13 +18,14 @@
|
|
|
17
18
|
interface Props {
|
|
18
19
|
resourceName: string;
|
|
19
20
|
id?: string | number;
|
|
20
|
-
mode?: 'create' | 'edit' | 'clone';
|
|
21
|
+
mode?: 'create' | 'edit' | 'clone' | 'show';
|
|
21
22
|
fieldRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; onchange: (v: unknown) => void }]>;
|
|
22
23
|
formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
|
|
23
24
|
headerContent?: Snippet;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent }: Props = $props();
|
|
28
|
+
const isReadonly = $derived(mode === 'show');
|
|
27
29
|
|
|
28
30
|
// ─── Resource metadata ────────────────────────────────────────────
|
|
29
31
|
const resource = $derived(getResource(resourceName));
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
if (f.showInForm === false) return false;
|
|
35
37
|
if (mode === 'create' && f.showInCreate === false) return false;
|
|
36
38
|
if (mode === 'edit' && f.showInEdit === false) return false;
|
|
39
|
+
if (mode === 'show' && f.showInShow === false) return false;
|
|
37
40
|
return true;
|
|
38
41
|
}));
|
|
39
42
|
|
|
@@ -71,6 +74,18 @@
|
|
|
71
74
|
|
|
72
75
|
const validator = $derived(deriveValidator(formFields));
|
|
73
76
|
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (mode !== 'create') return;
|
|
79
|
+
if (!form.isTainted()) {
|
|
80
|
+
for (const key in defaults) {
|
|
81
|
+
if (form.values[key] !== defaults[key]) {
|
|
82
|
+
form.setFieldValue(key, defaults[key]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
form.untaint();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
74
89
|
// ─── useForm: single source of truth for values, errors, tainted ──
|
|
75
90
|
const form = useForm({
|
|
76
91
|
get resource() { return resourceName; },
|
|
@@ -97,6 +112,8 @@
|
|
|
97
112
|
const pageTitle = $derived(
|
|
98
113
|
mode === 'create'
|
|
99
114
|
? `${t('common.create')}${resource.label}`
|
|
115
|
+
: mode === 'show'
|
|
116
|
+
? `${t('common.detail')}${resource.label}`
|
|
100
117
|
: `${t('common.edit')}${resource.label}`
|
|
101
118
|
);
|
|
102
119
|
|
|
@@ -153,10 +170,12 @@
|
|
|
153
170
|
{:else}
|
|
154
171
|
<form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-3xl space-y-6">
|
|
155
172
|
{#if submitError}
|
|
156
|
-
<
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
<div transition:slide={{ duration: 300, axis: 'y' }} class="svadmin-shake">
|
|
174
|
+
<Alert.Root variant="destructive">
|
|
175
|
+
<AlertCircle class="h-4 w-4" />
|
|
176
|
+
<Alert.Description>{submitError}</Alert.Description>
|
|
177
|
+
</Alert.Root>
|
|
178
|
+
</div>
|
|
160
179
|
{/if}
|
|
161
180
|
|
|
162
181
|
{#if hasGroups}
|
|
@@ -177,10 +196,11 @@
|
|
|
177
196
|
{field}
|
|
178
197
|
value={form.values[field.key]}
|
|
179
198
|
onchange={(val: unknown) => form.setFieldValue(field.key, val)}
|
|
199
|
+
disabled={isReadonly}
|
|
180
200
|
/>
|
|
181
201
|
{/if}
|
|
182
202
|
{#if form.errors[field.key]}
|
|
183
|
-
<p class="text-destructive text-[0.8125rem] mt-1">{form.errors[field.key]}</p>
|
|
203
|
+
<p class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
|
|
184
204
|
{/if}
|
|
185
205
|
</div>
|
|
186
206
|
{/each}
|
|
@@ -199,10 +219,11 @@
|
|
|
199
219
|
{field}
|
|
200
220
|
value={form.values[field.key]}
|
|
201
221
|
onchange={(val: unknown) => form.setFieldValue(field.key, val)}
|
|
222
|
+
disabled={isReadonly}
|
|
202
223
|
/>
|
|
203
224
|
{/if}
|
|
204
225
|
{#if form.errors[field.key]}
|
|
205
|
-
<p class="text-destructive text-[0.8125rem] mt-1">{form.errors[field.key]}</p>
|
|
226
|
+
<p class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
|
|
206
227
|
{/if}
|
|
207
228
|
</div>
|
|
208
229
|
{/each}
|
|
@@ -213,7 +234,7 @@
|
|
|
213
234
|
<div class="flex items-center gap-3">
|
|
214
235
|
{#if formActions}
|
|
215
236
|
{@render formActions({ isLoading: form.submitting, onSubmit: handleSubmit })}
|
|
216
|
-
{:else}
|
|
237
|
+
{:else if !isReadonly}
|
|
217
238
|
<Button type="submit" disabled={form.submitting}>
|
|
218
239
|
{#if form.submitting}
|
|
219
240
|
<Loader2 class="h-4 w-4 animate-spin" data-icon="inline-start" />
|
|
@@ -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 } from '@svadmin/core';
|
|
14
|
+
import { useList, useDelete, 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';
|
|
@@ -146,7 +146,6 @@
|
|
|
146
146
|
} as any);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
// Add popover filters
|
|
150
149
|
for (const [key, value] of Object.entries(filterValues)) {
|
|
151
150
|
if (value.trim()) {
|
|
152
151
|
result.push({ field: key, operator: 'contains', value: value.trim() });
|
|
@@ -155,6 +154,12 @@
|
|
|
155
154
|
return result;
|
|
156
155
|
});
|
|
157
156
|
|
|
157
|
+
$effect(() => {
|
|
158
|
+
if (Object.values(filterValues).some(v => v.trim())) {
|
|
159
|
+
pagination = { ...pagination, current: 1 };
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
158
163
|
// ─── Data fetching ────────────────────────────────────────────
|
|
159
164
|
const listResult = useList({
|
|
160
165
|
get resource() { return resourceName; },
|
|
@@ -222,6 +227,14 @@
|
|
|
222
227
|
}
|
|
223
228
|
});
|
|
224
229
|
|
|
230
|
+
// Sync external sorters → TanStack sorting state
|
|
231
|
+
$effect(() => {
|
|
232
|
+
const newSorting = sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }));
|
|
233
|
+
if (JSON.stringify(newSorting) !== JSON.stringify(sorting)) {
|
|
234
|
+
sorting = newSorting;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
225
238
|
// ─── Auto-generate columns from resource fields ──────────────
|
|
226
239
|
const visibleFields = $derived(
|
|
227
240
|
resource.fields.filter(f => f.showInList !== false)
|
|
@@ -229,7 +242,7 @@
|
|
|
229
242
|
|
|
230
243
|
const columns = $derived<ColumnDef<any, Record<string, unknown>>[]>([
|
|
231
244
|
// Selection column
|
|
232
|
-
...(selectable && canDelete ? [{
|
|
245
|
+
...(selectable && (canDelete || batchActions) ? [{
|
|
233
246
|
id: '_select',
|
|
234
247
|
header: () => '',
|
|
235
248
|
cell: () => '',
|
|
@@ -291,7 +304,7 @@
|
|
|
291
304
|
onColumnOrderChange: (updater: any) => {
|
|
292
305
|
columnOrder = typeof updater === 'function' ? updater(columnOrder) : updater;
|
|
293
306
|
},
|
|
294
|
-
get enableRowSelection() { return selectable && canDelete; },
|
|
307
|
+
get enableRowSelection() { return selectable && (canDelete || batchActions); },
|
|
295
308
|
get enableExpanding() { return !!expandedRowRender; },
|
|
296
309
|
});
|
|
297
310
|
|
|
@@ -334,7 +347,14 @@
|
|
|
334
347
|
const ids = Object.keys(rowSelection);
|
|
335
348
|
confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
|
|
336
349
|
confirmAction = async () => {
|
|
337
|
-
await Promise.allSettled(ids.map(id => deleteMutation.mutateAsync({ id, resource: resourceName })));
|
|
350
|
+
const results = await Promise.allSettled(ids.map(id => deleteMutation.mutateAsync({ id, resource: resourceName, successNotification: false })));
|
|
351
|
+
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
|
352
|
+
const failed = results.filter(r => r.status === 'rejected');
|
|
353
|
+
if (failed.length > 0) {
|
|
354
|
+
notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: failed.length, total: ids.length }) });
|
|
355
|
+
} else {
|
|
356
|
+
notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: succeeded }) });
|
|
357
|
+
}
|
|
338
358
|
rowSelection = {};
|
|
339
359
|
confirmOpen = false;
|
|
340
360
|
};
|
|
@@ -465,7 +485,7 @@
|
|
|
465
485
|
value={filterValues[field.key] ?? ''}
|
|
466
486
|
onchange={(e) => filterValues[field.key] = (e.currentTarget as HTMLSelectElement).value}
|
|
467
487
|
>
|
|
468
|
-
<option value=""
|
|
488
|
+
<option value="">{t('common.all')}</option>
|
|
469
489
|
{#each field.options as opt}
|
|
470
490
|
<option value={opt.value}>{opt.label}</option>
|
|
471
491
|
{/each}
|
|
@@ -500,7 +520,7 @@
|
|
|
500
520
|
</div>
|
|
501
521
|
|
|
502
522
|
<!-- Table (TanStack-powered) -->
|
|
503
|
-
<div class="rounded-
|
|
523
|
+
<div class="rounded-[24px] bg-card/80 backdrop-blur-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] overflow-hidden ring-1 ring-border/30" role="region" aria-label="{resource.label} {t('common.list')}">
|
|
504
524
|
{#if query.isLoading}
|
|
505
525
|
<div class="p-4 space-y-3">
|
|
506
526
|
<div class="flex gap-4 mb-2">
|
|
@@ -536,7 +556,7 @@
|
|
|
536
556
|
{@const header = col.header as typeof headerGroup.headers[0]}
|
|
537
557
|
<Table.Head
|
|
538
558
|
{...dragProps}
|
|
539
|
-
class={cn("bg-
|
|
559
|
+
class={cn("bg-transparent hover:bg-muted/10 border-b border-border/20 uppercase tracking-[0.12em] text-[10px] text-muted-foreground/70 font-semibold py-4", dragProps.class)}
|
|
540
560
|
style={header.getSize() !== 150 ? `width:${header.getSize()}px` : undefined}
|
|
541
561
|
>
|
|
542
562
|
{#if header.id === '_select'}
|
|
@@ -552,7 +572,7 @@
|
|
|
552
572
|
<Button
|
|
553
573
|
variant="ghost"
|
|
554
574
|
size="sm"
|
|
555
|
-
class="flex items-center gap-1 hover:text-foreground -ml-
|
|
575
|
+
class="flex items-center gap-1 hover:text-foreground -ml-2 h-auto py-1 px-2 uppercase tracking-wide text-[0.7rem] font-semibold"
|
|
556
576
|
onclick={() => header.column.toggleSorting()}
|
|
557
577
|
>
|
|
558
578
|
{visibleFields.find(f => f.key === header.id)?.label ?? header.id}
|
|
@@ -578,7 +598,7 @@
|
|
|
578
598
|
<ContextMenu.Root>
|
|
579
599
|
<ContextMenu.Trigger>
|
|
580
600
|
{#snippet child({ props })}
|
|
581
|
-
<Table.Row {...props} class="transition-
|
|
601
|
+
<Table.Row {...props} class="transition-all duration-300 border-b border-border/10 {row.getIsSelected() ? 'bg-primary/5' : 'hover:bg-muted/20'}">
|
|
582
602
|
{#each row.getVisibleCells() as cell}
|
|
583
603
|
<Table.Cell>
|
|
584
604
|
{#if cell.column.id === '_select'}
|
|
@@ -670,7 +690,7 @@
|
|
|
670
690
|
</ContextMenu.Content>
|
|
671
691
|
</ContextMenu.Root>
|
|
672
692
|
{#if expandedRowRender && row.getIsExpanded()}
|
|
673
|
-
<Table.Row class="bg-muted/
|
|
693
|
+
<Table.Row class="bg-muted/10 border-b border-border/10 transition-all">
|
|
674
694
|
<Table.Cell colspan={row.getVisibleCells().length}>
|
|
675
695
|
{@render expandedRowRender({ record })}
|
|
676
696
|
</Table.Cell>
|
|
@@ -709,12 +729,12 @@
|
|
|
709
729
|
{@const record = row.original}
|
|
710
730
|
{@const id = record[primaryKey] as string | number}
|
|
711
731
|
<div
|
|
712
|
-
class="rounded-
|
|
732
|
+
class="rounded-[20px] shadow-[0_4px_20px_rgb(0,0,0,0.03)] ring-1 ring-border/20 bg-card p-5 transition-all {row.getIsSelected() ? 'ring-2 ring-primary/50 bg-primary/5' : ''}"
|
|
713
733
|
>
|
|
714
734
|
<!-- Card header: ID + select + actions -->
|
|
715
735
|
<div class="flex items-center justify-between mb-3">
|
|
716
736
|
<div class="flex items-center gap-2">
|
|
717
|
-
{#if selectable && canDelete}
|
|
737
|
+
{#if selectable && (canDelete || batchActions)}
|
|
718
738
|
<Checkbox
|
|
719
739
|
checked={row.getIsSelected()}
|
|
720
740
|
onCheckedChange={() => row.toggleSelected()}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
let { open = $bindable(false), onAskAI }: Props = $props();
|
|
17
17
|
let searchValue = $state('');
|
|
18
18
|
|
|
19
|
-
const resources = getResources();
|
|
19
|
+
const resources = $derived(getResources());
|
|
20
20
|
const hasAI = $derived(!!(getAgentProvider() || getChatProvider()));
|
|
21
21
|
|
|
22
22
|
function close() {
|
|
@@ -25,9 +25,23 @@
|
|
|
25
25
|
const resolvedTitle = $derived(title ?? t('common.confirmAction'));
|
|
26
26
|
const resolvedConfirmText = $derived(confirmText ?? t('common.confirm'));
|
|
27
27
|
const resolvedCancelText = $derived(cancelText ?? t('common.cancel'));
|
|
28
|
+
|
|
29
|
+
let confirmed = $state(false);
|
|
30
|
+
|
|
31
|
+
function handleConfirm() {
|
|
32
|
+
confirmed = true;
|
|
33
|
+
onconfirm();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleOpenChange(v: boolean) {
|
|
37
|
+
if (!v && !confirmed) {
|
|
38
|
+
oncancel();
|
|
39
|
+
}
|
|
40
|
+
if (!v) confirmed = false;
|
|
41
|
+
}
|
|
28
42
|
</script>
|
|
29
43
|
|
|
30
|
-
<AlertDialog.Root bind:open onOpenChange={
|
|
44
|
+
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
|
|
31
45
|
<AlertDialog.Content>
|
|
32
46
|
<AlertDialog.Header>
|
|
33
47
|
<AlertDialog.Title>{resolvedTitle}</AlertDialog.Title>
|
|
@@ -43,7 +57,7 @@
|
|
|
43
57
|
</AlertDialog.Cancel>
|
|
44
58
|
<AlertDialog.Action>
|
|
45
59
|
{#snippet child({ props })}
|
|
46
|
-
<Button variant={variantMap[variant as keyof typeof variantMap]} {...props} onclick={
|
|
60
|
+
<Button variant={variantMap[variant as keyof typeof variantMap]} {...props} onclick={handleConfirm}>
|
|
47
61
|
{resolvedConfirmText}
|
|
48
62
|
</Button>
|
|
49
63
|
{/snippet}
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
import type { Snippet } from 'svelte';
|
|
21
21
|
import { getRichTextEditor } from '../editor-config.svelte.js';
|
|
22
22
|
|
|
23
|
-
let { field, value, onchange, children } = $props<{
|
|
23
|
+
let { field, value, onchange, disabled, children } = $props<{
|
|
24
24
|
field: FieldDefinition;
|
|
25
25
|
value: unknown;
|
|
26
26
|
onchange: (val: unknown) => void;
|
|
27
|
-
|
|
27
|
+
disabled?: boolean;
|
|
28
28
|
children?: Snippet;
|
|
29
29
|
}>();
|
|
30
30
|
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
value={strVal}
|
|
99
99
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
100
100
|
required={field.required}
|
|
101
|
+
{disabled}
|
|
101
102
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
102
103
|
/>
|
|
103
104
|
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
value={strVal}
|
|
109
110
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
110
111
|
required={field.required}
|
|
112
|
+
{disabled}
|
|
111
113
|
placeholder="name@example.com"
|
|
112
114
|
/>
|
|
113
115
|
|
|
@@ -118,6 +120,7 @@
|
|
|
118
120
|
value={strVal}
|
|
119
121
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
120
122
|
required={field.required}
|
|
123
|
+
{disabled}
|
|
121
124
|
placeholder="https://"
|
|
122
125
|
/>
|
|
123
126
|
|
|
@@ -128,6 +131,7 @@
|
|
|
128
131
|
value={strVal}
|
|
129
132
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
130
133
|
required={field.required}
|
|
134
|
+
{disabled}
|
|
131
135
|
placeholder="+1 (555) 000-0000"
|
|
132
136
|
/>
|
|
133
137
|
|
|
@@ -139,6 +143,7 @@
|
|
|
139
143
|
value={strVal || '#000000'}
|
|
140
144
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
141
145
|
class="h-10 w-14 cursor-pointer rounded-md border border-input bg-background p-1"
|
|
146
|
+
disabled={disabled}
|
|
142
147
|
/>
|
|
143
148
|
<Input
|
|
144
149
|
type="text"
|
|
@@ -146,6 +151,7 @@
|
|
|
146
151
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
147
152
|
placeholder="#000000"
|
|
148
153
|
class="max-w-32 font-mono text-sm"
|
|
154
|
+
{disabled}
|
|
149
155
|
/>
|
|
150
156
|
{#if strVal}
|
|
151
157
|
<span
|
|
@@ -160,8 +166,12 @@
|
|
|
160
166
|
id={field.key}
|
|
161
167
|
type="number"
|
|
162
168
|
value={String(numVal)}
|
|
163
|
-
oninput={(e) =>
|
|
169
|
+
oninput={(e) => {
|
|
170
|
+
const v = (e.target as HTMLInputElement).value;
|
|
171
|
+
onchange(v === '' ? null : Number(v));
|
|
172
|
+
}}
|
|
164
173
|
required={field.required}
|
|
174
|
+
{disabled}
|
|
165
175
|
/>
|
|
166
176
|
|
|
167
177
|
{:else if field.type === 'richtext'}
|
|
@@ -173,6 +183,7 @@
|
|
|
173
183
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
174
184
|
preset="full"
|
|
175
185
|
onchange={(html: string) => onchange(html)}
|
|
186
|
+
{disabled}
|
|
176
187
|
/>
|
|
177
188
|
{:else}
|
|
178
189
|
<Textarea
|
|
@@ -180,6 +191,7 @@
|
|
|
180
191
|
value={strVal}
|
|
181
192
|
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
182
193
|
required={field.required}
|
|
194
|
+
{disabled}
|
|
183
195
|
rows={10}
|
|
184
196
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
185
197
|
class="resize-y"
|
|
@@ -192,6 +204,7 @@
|
|
|
192
204
|
value={strVal}
|
|
193
205
|
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
194
206
|
required={field.required}
|
|
207
|
+
{disabled}
|
|
195
208
|
rows={4}
|
|
196
209
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
197
210
|
class="resize-y"
|
|
@@ -206,24 +219,31 @@
|
|
|
206
219
|
optionLabel={field.optionLabel ?? 'title'}
|
|
207
220
|
optionValue={field.optionValue ?? 'id'}
|
|
208
221
|
placeholder={t('field.selectPlaceholder')}
|
|
222
|
+
{disabled}
|
|
209
223
|
/>
|
|
210
224
|
|
|
211
225
|
{:else if field.type === 'select'}
|
|
212
226
|
{#if (field.options?.length ?? 0) > 8}
|
|
213
|
-
|
|
214
|
-
<ComboboxField
|
|
227
|
+
<select
|
|
215
228
|
id={field.key}
|
|
216
|
-
|
|
217
|
-
value={
|
|
218
|
-
onchange={(
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
230
|
+
value={strVal}
|
|
231
|
+
onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
|
|
232
|
+
required={field.required}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
>
|
|
235
|
+
<option value="">{t('field.selectPlaceholder')}</option>
|
|
236
|
+
{#each field.options ?? [] as opt}
|
|
237
|
+
<option value={opt.value}>{opt.label}</option>
|
|
238
|
+
{/each}
|
|
239
|
+
</select>
|
|
221
240
|
{:else}
|
|
222
241
|
<Select
|
|
223
242
|
id={field.key}
|
|
224
243
|
value={strVal}
|
|
225
244
|
onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
|
|
226
245
|
required={field.required}
|
|
246
|
+
{disabled}
|
|
227
247
|
placeholder={t('field.selectPlaceholder')}
|
|
228
248
|
>
|
|
229
249
|
{#each field.options ?? [] as opt}
|
|
@@ -244,6 +264,7 @@
|
|
|
244
264
|
id={`${field.key}-${opt.value}`}
|
|
245
265
|
checked={multiVal.includes(opt.value)}
|
|
246
266
|
onCheckedChange={() => toggleMulti(opt.value)}
|
|
267
|
+
disabled={disabled}
|
|
247
268
|
/>
|
|
248
269
|
{opt.label}
|
|
249
270
|
</label>
|
|
@@ -270,6 +291,7 @@
|
|
|
270
291
|
id={field.key}
|
|
271
292
|
checked={boolVal}
|
|
272
293
|
onCheckedChange={(v) => onchange(v)}
|
|
294
|
+
disabled={disabled}
|
|
273
295
|
/>
|
|
274
296
|
<span class="text-sm text-muted-foreground">{boolVal ? t('common.yes') : t('common.no')}</span>
|
|
275
297
|
</div>
|
|
@@ -294,6 +316,7 @@
|
|
|
294
316
|
type="text"
|
|
295
317
|
placeholder={t('field.tagsPlaceholder')}
|
|
296
318
|
onkeydown={handleTagKeydown}
|
|
319
|
+
{disabled}
|
|
297
320
|
/>
|
|
298
321
|
</div>
|
|
299
322
|
|
|
@@ -304,6 +327,7 @@
|
|
|
304
327
|
value={strVal}
|
|
305
328
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
306
329
|
required={field.required}
|
|
330
|
+
{disabled}
|
|
307
331
|
/>
|
|
308
332
|
|
|
309
333
|
{:else if field.type === 'images'}
|
|
@@ -317,6 +341,7 @@
|
|
|
317
341
|
oninput={(e) => updateImage(i, (e.target as HTMLInputElement).value)}
|
|
318
342
|
placeholder="https://example.com/image.jpg"
|
|
319
343
|
class="flex-1"
|
|
344
|
+
{disabled}
|
|
320
345
|
/>
|
|
321
346
|
{#if url}
|
|
322
347
|
<img src={url} alt="preview" class="h-9 w-9 rounded object-cover border" />
|
|
@@ -342,6 +367,7 @@
|
|
|
342
367
|
// keep raw text until valid JSON
|
|
343
368
|
}
|
|
344
369
|
}}
|
|
370
|
+
{disabled}
|
|
345
371
|
rows={6}
|
|
346
372
|
class="resize-y font-mono text-xs"
|
|
347
373
|
/>
|
|
@@ -352,6 +378,7 @@
|
|
|
352
378
|
type="text"
|
|
353
379
|
value={strVal}
|
|
354
380
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
381
|
+
{disabled}
|
|
355
382
|
/>
|
|
356
383
|
{/if}
|
|
357
384
|
</div>
|
|
@@ -5,7 +5,7 @@ type $$ComponentProps = {
|
|
|
5
5
|
field: FieldDefinition;
|
|
6
6
|
value: unknown;
|
|
7
7
|
onchange: (val: unknown) => void;
|
|
8
|
-
|
|
8
|
+
disabled?: boolean;
|
|
9
9
|
children?: Snippet;
|
|
10
10
|
};
|
|
11
11
|
declare const FieldRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -26,12 +26,11 @@
|
|
|
26
26
|
siteUrl?: string;
|
|
27
27
|
}>();
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const CustomNotificationPanel = registry.NotificationPanel;
|
|
29
|
+
const registry = $derived(getComponentRegistry() || {} as any);
|
|
30
|
+
const CustomBreadcrumbs = $derived(registry.Breadcrumbs);
|
|
31
|
+
const CustomThemeToggle = $derived(registry.ThemeToggle);
|
|
32
|
+
const CustomUserMenu = $derived(registry.UserMenu);
|
|
33
|
+
const CustomNotificationPanel = $derived(registry.NotificationPanel);
|
|
35
34
|
</script>
|
|
36
35
|
|
|
37
36
|
<header class="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between bg-card/60 backdrop-blur-md px-4 md:px-6">
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { fade, fly } from 'svelte/transition';
|
|
4
4
|
import Sidebar from './Sidebar.svelte';
|
|
5
|
-
import Toast from './Toast.svelte';
|
|
6
5
|
import Header from './Header.svelte';
|
|
7
6
|
import CommandPalette from './CommandPalette.svelte';
|
|
8
7
|
import KeyboardShortcuts from './KeyboardShortcuts.svelte';
|
|
@@ -168,5 +167,4 @@
|
|
|
168
167
|
/>
|
|
169
168
|
<KeyboardShortcuts bind:open={shortcutsOpen} />
|
|
170
169
|
<ChatDialog />
|
|
171
|
-
<Toast />
|
|
172
170
|
{/if}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
const { menuItems } = useMenu();
|
|
6
6
|
|
|
7
|
+
let navigated = false;
|
|
7
8
|
$effect(() => {
|
|
8
|
-
//
|
|
9
|
-
if (menuItems.length > 0) {
|
|
9
|
+
// Navigate to first resource ONCE — avoid infinite re-triggers
|
|
10
|
+
if (!navigated && menuItems.length > 0) {
|
|
11
|
+
navigated = true;
|
|
10
12
|
navigate(menuItems[0].route);
|
|
11
13
|
}
|
|
12
14
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
isGranted,
|
|
16
16
|
|
|
17
17
|
// Interactive states
|
|
18
|
-
selectedRole = $bindable(
|
|
18
|
+
selectedRole = $bindable(''),
|
|
19
19
|
loading = false,
|
|
20
20
|
|
|
21
21
|
// Feedback and alerts (passed down from upper business logic)
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
onToggle: (role: string, resource: string, action: string, grant: boolean) => void;
|
|
45
45
|
} = $props();
|
|
46
46
|
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (!selectedRole && roles.length > 0) {
|
|
49
|
+
selectedRole = roles[0].code;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
47
52
|
</script>
|
|
48
53
|
|
|
49
54
|
<div class="h-full flex flex-col pt-2">
|
|
@@ -73,5 +73,14 @@
|
|
|
73
73
|
{/each}
|
|
74
74
|
</Card.Content>
|
|
75
75
|
</Card.Root>
|
|
76
|
+
{:else}
|
|
77
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
78
|
+
<Card.Content class="p-8 text-center">
|
|
79
|
+
<p class="text-muted-foreground">{t('common.noData')}</p>
|
|
80
|
+
{#if query.isError}
|
|
81
|
+
<p class="text-sm text-destructive mt-2">{(query.error as Error)?.message ?? t('common.operationFailed')}</p>
|
|
82
|
+
{/if}
|
|
83
|
+
</Card.Content>
|
|
84
|
+
</Card.Root>
|
|
76
85
|
{/if}
|
|
77
86
|
</div>
|
|
@@ -80,10 +80,11 @@
|
|
|
80
80
|
let navItems = $state.raw<NavItem[]>([]);
|
|
81
81
|
|
|
82
82
|
$effect(() => {
|
|
83
|
-
// Re-run when locale or resources change
|
|
84
83
|
const localeVal = getLocale();
|
|
84
|
+
const currentResources = getResources();
|
|
85
|
+
let cancelled = false;
|
|
85
86
|
|
|
86
|
-
Promise.all(
|
|
87
|
+
Promise.all(currentResources.map(async (r) => {
|
|
87
88
|
try {
|
|
88
89
|
const { can } = await canAccessAsync(r.name, 'list');
|
|
89
90
|
return { r, can };
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
return { r, can: false };
|
|
92
93
|
}
|
|
93
94
|
})).then(results => {
|
|
95
|
+
if (cancelled) return;
|
|
94
96
|
const items: NavItem[] = [{ path: '/', label: t('common.home'), Icon: LayoutDashboard }];
|
|
95
97
|
for (const { r, can } of results) {
|
|
96
98
|
if (can) {
|
|
@@ -104,6 +106,8 @@
|
|
|
104
106
|
}
|
|
105
107
|
navItems = items;
|
|
106
108
|
});
|
|
109
|
+
|
|
110
|
+
return () => { cancelled = true; };
|
|
107
111
|
});
|
|
108
112
|
|
|
109
113
|
/** Toggle between available locales */
|
|
@@ -254,9 +258,10 @@
|
|
|
254
258
|
{@const active = isActive(item.path)}
|
|
255
259
|
<a
|
|
256
260
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
257
|
-
|
|
261
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
262
|
+
class="flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-300
|
|
258
263
|
{active
|
|
259
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
264
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
|
|
260
265
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
261
266
|
>
|
|
262
267
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -277,10 +282,11 @@
|
|
|
277
282
|
<a
|
|
278
283
|
{...props}
|
|
279
284
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
280
|
-
|
|
285
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
286
|
+
class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
|
|
281
287
|
{active
|
|
282
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
283
|
-
: 'text-sidebar-foreground/
|
|
288
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20'
|
|
289
|
+
: 'text-sidebar-foreground/60 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground'}"
|
|
284
290
|
>
|
|
285
291
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
286
292
|
</a>
|
|
@@ -293,9 +299,10 @@
|
|
|
293
299
|
{:else}
|
|
294
300
|
<a
|
|
295
301
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
296
|
-
|
|
302
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
303
|
+
class="flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
|
|
297
304
|
{active
|
|
298
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
305
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
|
|
299
306
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
300
307
|
>
|
|
301
308
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
103
103
|
class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
104
104
|
{active
|
|
105
|
-
? 'bg-
|
|
105
|
+
? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5'
|
|
106
106
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
107
107
|
>
|
|
108
108
|
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
122
122
|
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
|
|
123
123
|
{active
|
|
124
|
-
? 'bg-
|
|
124
|
+
? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5 font-semibold before:absolute before:left-0 before:top-2 before:bottom-2 before:w-[3px] before:bg-primary before:rounded-r-md overflow-hidden relative'
|
|
125
125
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
126
126
|
style="padding-left: {12 + depth * 12}px"
|
|
127
127
|
>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
9
|
tenants = [],
|
|
10
|
-
currentTenantId = $bindable(
|
|
10
|
+
currentTenantId = $bindable(undefined as string | undefined),
|
|
11
11
|
collapsed = false,
|
|
12
12
|
onSwitch,
|
|
13
13
|
headerSnippet,
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
{/if}
|
|
75
75
|
</div>
|
|
76
76
|
<span class="flex-1 truncate">{tenant.name}</span>
|
|
77
|
-
{#if tenant.id ===
|
|
77
|
+
{#if tenant.id === currentTenant?.id}
|
|
78
78
|
<Check class="h-4 w-4 text-primary" />
|
|
79
79
|
{/if}
|
|
80
80
|
</DropdownMenu.Item>
|
|
@@ -89,6 +89,18 @@
|
|
|
89
89
|
isListening = false;
|
|
90
90
|
if (onend) onend();
|
|
91
91
|
};
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
if (recognition) {
|
|
95
|
+
isListening = false;
|
|
96
|
+
recognition.onend = () => {};
|
|
97
|
+
recognition.stop();
|
|
98
|
+
recognition.onstart = null;
|
|
99
|
+
recognition.onresult = null;
|
|
100
|
+
recognition.onerror = null;
|
|
101
|
+
recognition.onend = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
92
104
|
});
|
|
93
105
|
|
|
94
106
|
function toggleListening(e?: MouseEvent) {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
bind:ref
|
|
29
29
|
data-slot="dialog-content"
|
|
30
30
|
class={cn(
|
|
31
|
-
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-
|
|
31
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-98 data-[state=open]:zoom-in-98 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-6 text-sm ring-1 duration-300 sm:max-w-lg fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none shadow-2xl",
|
|
32
32
|
className
|
|
33
33
|
)}
|
|
34
34
|
{...restProps}
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
<DialogPrimitive.Overlay
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="dialog-overlay"
|
|
15
|
-
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/
|
|
15
|
+
class={cn("data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/20 duration-200 backdrop-blur-sm fixed inset-0 isolate z-50", className)}
|
|
16
16
|
{...restProps}
|
|
17
17
|
/>
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
...restProps
|
|
16
16
|
}: Props = $props();
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const getOpen = getContext<() => boolean>("svadmin-dropdown-open");
|
|
19
|
+
let isOpen = $derived(getOpen?.() ?? true);
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
22
|
{#if isOpen}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"./components/*": "./dist/components/*"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@svadmin/core": "^0.
|
|
28
|
+
"@svadmin/core": "^0.23.0",
|
|
29
29
|
"@tanstack/svelte-query": "^6.0.0",
|
|
30
30
|
"highlight.js": "^11.11.1",
|
|
31
31
|
"isomorphic-dompurify": "^3.7.1",
|