@svadmin/ui 0.38.7 → 0.40.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 +0 -4
- package/dist/app.theme.css +0 -4
- package/dist/assets/nft/nft-cover-01.jpg +0 -0
- package/dist/assets/nft/nft-cover-02.jpg +0 -0
- package/dist/assets/nft/nft-cover-03.jpg +0 -0
- package/dist/assets/nft/nft-cover-04.jpg +0 -0
- package/dist/assets/nft/nft-cover-05.jpg +0 -0
- package/dist/assets/nft/nft-cover-06.jpg +0 -0
- package/dist/assets/nft/nft-cover-07.jpg +0 -0
- package/dist/assets/nft/nft-cover-08.jpg +0 -0
- package/dist/assets/nft/nft-cover-09.jpg +0 -0
- package/dist/components/AdminApp.svelte +29 -12
- package/dist/components/ApiSettings.svelte +86 -1
- package/dist/components/AutoTable.svelte +40 -13
- package/dist/components/Breadcrumbs.svelte +39 -6
- package/dist/components/Breadcrumbs.svelte.d.ts +5 -1
- package/dist/components/ChatDialog.svelte +4 -2
- package/dist/components/ChatDialog.svelte.d.ts +2 -0
- package/dist/components/DevTools.svelte +3 -1
- package/dist/components/DevTools.svelte.d.ts +4 -1
- package/dist/components/ForgotPasswordPage.svelte +5 -8
- package/dist/components/Header.svelte +5 -3
- package/dist/components/Header.svelte.d.ts +2 -0
- package/dist/components/Layout.svelte +10 -1
- package/dist/components/LoginPage.svelte +5 -8
- package/dist/components/RegisterPage.svelte +5 -8
- package/dist/components/SettingsPage.svelte +14 -2
- package/dist/components/Sidebar.svelte +10 -4
- package/dist/components/SvadminLogo.svelte +16 -0
- package/dist/components/SvadminLogo.svelte.d.ts +7 -0
- package/dist/components/SystemPageShell.svelte +34 -0
- package/dist/components/SystemPageShell.svelte.d.ts +8 -0
- package/dist/components/UpdatePasswordPage.svelte +5 -8
- package/dist/components/account/CompanyProfilePage.svelte +56 -1
- package/dist/components/account/ImportMembersPage.svelte +31 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +172 -19
- package/dist/components/account/SettingsPlainPage.svelte +121 -17
- package/dist/components/account/SettingsSidebarPage.svelte +282 -70
- package/dist/components/account/TeamMembersPage.svelte +95 -4
- package/dist/components/account/UserProfilePage.svelte +206 -0
- package/dist/components/account/UserProfilePage.svelte.d.ts +4 -0
- package/dist/components/network/TeamCrewTablePage.svelte +37 -5
- package/dist/components/network/UserCardsNFTPage.svelte +26 -16
- package/dist/components/profile/ActivityTimeline.svelte +24 -2
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +1 -0
- package/dist/components/profile/ProfileVariantSections.svelte +452 -0
- package/dist/components/profile/ProfileVariantSections.svelte.d.ts +8 -0
- package/dist/components/profile/ProjectsGrid.svelte +38 -8
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +1 -0
- package/dist/components/profile/PublicProfilePage.svelte +47 -26
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +1 -0
- package/dist/components/profile/TeamsShowcase.svelte +31 -12
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +1 -0
- package/dist/components/ui/alert/alert.svelte.d.ts +14 -14
- package/dist/components/ui/avatar/avatar.svelte.d.ts +14 -14
- package/dist/components/ui/badge/badge.svelte.d.ts +20 -20
- package/dist/components/ui/button/button.svelte.d.ts +44 -44
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/router-state.svelte.js +4 -0
- package/package.json +15 -15
package/dist/app.css
CHANGED
|
@@ -1363,10 +1363,6 @@ main {
|
|
|
1363
1363
|
padding-right: 1rem !important;
|
|
1364
1364
|
}
|
|
1365
1365
|
|
|
1366
|
-
.layout-clean-flat [data-svadmin-content-page] [class*="grid-cols-3"] {
|
|
1367
|
-
grid-template-columns: minmax(0, 1fr) !important;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
1366
|
.layout-clean-flat [data-svadmin-content-page="auth-2fa"] [data-slot="card-content"],
|
|
1371
1367
|
.layout-clean-flat [data-svadmin-content-page="error"] [data-slot="card-content"] {
|
|
1372
1368
|
padding-left: 1rem !important;
|
package/dist/app.theme.css
CHANGED
|
@@ -1362,10 +1362,6 @@ main {
|
|
|
1362
1362
|
padding-right: 1rem !important;
|
|
1363
1363
|
}
|
|
1364
1364
|
|
|
1365
|
-
.layout-clean-flat [data-svadmin-content-page] [class*="grid-cols-3"] {
|
|
1366
|
-
grid-template-columns: minmax(0, 1fr) !important;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
1365
|
.layout-clean-flat [data-svadmin-content-page="auth-2fa"] [data-slot="card-content"],
|
|
1370
1366
|
.layout-clean-flat [data-svadmin-content-page="error"] [data-slot="card-content"] {
|
|
1371
1367
|
padding-left: 1rem !important;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
const loadPublicProfilePage = () => import('./profile/PublicProfilePage.svelte');
|
|
3
3
|
const loadGetStartedPage = () => import('./account/GetStartedPage.svelte');
|
|
4
|
+
const loadUserProfilePage = () => import('./account/UserProfilePage.svelte');
|
|
4
5
|
const loadCompanyProfilePage = () => import('./account/CompanyProfilePage.svelte');
|
|
5
6
|
const loadSettingsPlainPage = () => import('./account/SettingsPlainPage.svelte');
|
|
6
7
|
const loadSettingsSidebarPage = () => import('./account/SettingsSidebarPage.svelte');
|
|
@@ -42,12 +43,12 @@
|
|
|
42
43
|
import ForgotPasswordPage from './ForgotPasswordPage.svelte';
|
|
43
44
|
import UpdatePasswordPage from './UpdatePasswordPage.svelte';
|
|
44
45
|
import ConfigErrorScreen from './ConfigErrorScreen.svelte';
|
|
45
|
-
import DevTools from './DevTools.svelte';
|
|
46
46
|
import Sidebar from './Sidebar.svelte';
|
|
47
47
|
import Header from './Header.svelte';
|
|
48
48
|
import TaskQueueDrawer from './TaskQueueDrawer.svelte';
|
|
49
49
|
import SettingsPage from './SettingsPage.svelte';
|
|
50
50
|
import ErrorPage from './ErrorPage.svelte';
|
|
51
|
+
import SystemPageShell from './SystemPageShell.svelte';
|
|
51
52
|
import LazyPage from './LazyPage.svelte';
|
|
52
53
|
import { Button } from './ui/button/index.js';
|
|
53
54
|
import { Input } from './ui/input/index.js';
|
|
@@ -235,6 +236,12 @@
|
|
|
235
236
|
const resourceNames = $derived(new Set(resources.map((resource) => resource.name)));
|
|
236
237
|
const hasRouteResource = $derived(!params.resource || resourceNames.has(params.resource));
|
|
237
238
|
const currentResourcePages = $derived(params.resource ? resourcePages?.[params.resource] : undefined);
|
|
239
|
+
const isStandaloneTwoFactor = $derived(route === '/2fa' || route === '/authentication/branded/2fa');
|
|
240
|
+
const standaloneErrorStatus = $derived.by((): '404' | '500' | undefined => {
|
|
241
|
+
if (route === '/authentication/error-404' || route === '/404' || (route === '/:resource' && params.resource === '404')) return '404';
|
|
242
|
+
if (route === '/authentication/error-500' || route === '/500' || (route === '/:resource' && params.resource === '500')) return '500';
|
|
243
|
+
return undefined;
|
|
244
|
+
});
|
|
238
245
|
|
|
239
246
|
// Auth check
|
|
240
247
|
let isAuthenticated = $state(untrack(() => !authProvider));
|
|
@@ -244,6 +251,13 @@
|
|
|
244
251
|
await adminContext.navigate(path);
|
|
245
252
|
}
|
|
246
253
|
|
|
254
|
+
function isPublicSystemRoute(currentRoute: string): boolean {
|
|
255
|
+
return currentRoute === '/authentication/error-404'
|
|
256
|
+
|| currentRoute === '/authentication/error-500'
|
|
257
|
+
|| currentRoute === '/404'
|
|
258
|
+
|| currentRoute === '/500';
|
|
259
|
+
}
|
|
260
|
+
|
|
247
261
|
$effect(() => {
|
|
248
262
|
let cancelled = false;
|
|
249
263
|
// Explicitly track route so auth is re-checked on navigation
|
|
@@ -261,7 +275,7 @@
|
|
|
261
275
|
if (cancelled) return;
|
|
262
276
|
isAuthenticated = result.authenticated;
|
|
263
277
|
authChecked = true;
|
|
264
|
-
if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
|
|
278
|
+
if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password' && !isPublicSystemRoute(_route)) {
|
|
265
279
|
void navigateWithinApp(result.redirectTo ?? '/login');
|
|
266
280
|
}
|
|
267
281
|
}).catch(err => {
|
|
@@ -269,7 +283,7 @@
|
|
|
269
283
|
console.warn('Auth check failed:', err);
|
|
270
284
|
isAuthenticated = false;
|
|
271
285
|
authChecked = true;
|
|
272
|
-
if (_route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
|
|
286
|
+
if (_route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password' && !isPublicSystemRoute(_route)) {
|
|
273
287
|
void navigateWithinApp('/login');
|
|
274
288
|
}
|
|
275
289
|
});
|
|
@@ -304,13 +318,20 @@
|
|
|
304
318
|
<UpdatePasswordPage {title} />
|
|
305
319
|
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
306
320
|
<ConfigErrorScreen title="{title} — {translation.t('common.configRequired')}" />
|
|
321
|
+
{:else if (isAuthenticated || !authProvider) && isStandaloneTwoFactor}
|
|
322
|
+
<SystemPageShell {title}>
|
|
323
|
+
<LazyPage loader={loadTwoFactorAuthPage} props={{}} />
|
|
324
|
+
</SystemPageShell>
|
|
325
|
+
{:else if standaloneErrorStatus}
|
|
326
|
+
<SystemPageShell {title}>
|
|
327
|
+
{@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
|
|
328
|
+
<ErrorComp status={standaloneErrorStatus} />
|
|
329
|
+
</SystemPageShell>
|
|
307
330
|
{:else if isAuthenticated || !authProvider}
|
|
308
331
|
<Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
|
|
309
332
|
{#key route + (params.resource ?? '') + (params.id ?? '') + (params.variant ?? '') + (params.columns ?? '')}
|
|
310
333
|
<div class="svadmin-page-enter">
|
|
311
|
-
{#if route === '/
|
|
312
|
-
<LazyPage loader={loadTwoFactorAuthPage} props={{}} />
|
|
313
|
-
{:else if route === '/public-profile'}
|
|
334
|
+
{#if route === '/public-profile'}
|
|
314
335
|
<LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects' }} />
|
|
315
336
|
{:else if route === '/public-profile/projects/:columns'}
|
|
316
337
|
<LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects', columns: params.columns?.includes('3') ? 3 : 2 }} />
|
|
@@ -319,11 +340,11 @@
|
|
|
319
340
|
{:else if route === '/public-profile/teams'}
|
|
320
341
|
<LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'teams' }} />
|
|
321
342
|
{:else if route === '/public-profile/profiles/:variant'}
|
|
322
|
-
<LazyPage loader={loadPublicProfilePage} props={{ variant: (params.variant === 'company' || params.variant === 'gamer' || params.variant === 'default' ? params.variant : 'default') as 'company' | 'gamer' | 'default' }} />
|
|
343
|
+
<LazyPage loader={loadPublicProfilePage} props={{ variant: (params.variant === 'company' || params.variant === 'gamer' || params.variant === 'default' ? params.variant : 'default') as 'company' | 'gamer' | 'default', showSections: true }} />
|
|
323
344
|
{:else if route === '/account/get-started' || route === '/account/home/get-started'}
|
|
324
345
|
<LazyPage loader={loadGetStartedPage} props={{}} />
|
|
325
346
|
{:else if route === '/account/home/user-profile'}
|
|
326
|
-
<
|
|
347
|
+
<LazyPage loader={loadUserProfilePage} props={{}} />
|
|
327
348
|
{:else if route === '/account/company-profile' || route === '/account/home/company-profile'}
|
|
328
349
|
<LazyPage loader={loadCompanyProfilePage} props={{}} />
|
|
329
350
|
{:else if route === '/account/settings-plain' || route === '/account/home/settings-plain'}
|
|
@@ -348,9 +369,6 @@
|
|
|
348
369
|
<LazyPage loader={loadTeamCrewTablePage} props={{}} />
|
|
349
370
|
{:else if route.startsWith('/settings')}
|
|
350
371
|
<SettingsPage />
|
|
351
|
-
{:else if route === '/500' || (route === '/:resource' && params.resource === '500')}
|
|
352
|
-
{@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
|
|
353
|
-
<ErrorComp status="500" />
|
|
354
372
|
{:else if route === '/' || route === '' || route === '/inventory-dashboard'}
|
|
355
373
|
{#if dashboard}
|
|
356
374
|
{@render dashboard()}
|
|
@@ -398,5 +416,4 @@
|
|
|
398
416
|
</div>
|
|
399
417
|
{/if}
|
|
400
418
|
<Toast />
|
|
401
|
-
<DevTools />
|
|
402
419
|
</QueryClientProvider>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
3
|
|
|
4
|
-
import { AlertCircle, Calendar, Check, Copy, Key, Plus, ShieldCheck, Trash2 } from '@lucide/svelte';
|
|
4
|
+
import { AlertCircle, Calendar, Check, Copy, Key, Plus, ShieldCheck, Trash2, Webhook } from '@lucide/svelte';
|
|
5
5
|
import * as Card from './ui/card/index.js';
|
|
6
6
|
import { Badge } from './ui/badge/index.js';
|
|
7
7
|
import { Button } from './ui/button/index.js';
|
|
@@ -30,6 +30,35 @@
|
|
|
30
30
|
let showCreatedToken = $state(false);
|
|
31
31
|
let copiedKeyId = $state<string | null>(null);
|
|
32
32
|
|
|
33
|
+
interface WebhookEntry {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
eventType: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let webhooks = $state<WebhookEntry[]>([
|
|
41
|
+
{ id: 'wh-1', name: 'Order Events', url: 'https://api.acme.com/hooks/orders', eventType: 'resource.updated' },
|
|
42
|
+
]);
|
|
43
|
+
let newWebhookName = $state('');
|
|
44
|
+
let newWebhookUrl = $state('');
|
|
45
|
+
let newWebhookEvent = $state('resource.created');
|
|
46
|
+
|
|
47
|
+
function handleAddWebhook(event: SubmitEvent) {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
if (!newWebhookName.trim() || !newWebhookUrl.trim()) return;
|
|
50
|
+
webhooks = [
|
|
51
|
+
...webhooks,
|
|
52
|
+
{ id: Date.now().toString(), name: newWebhookName.trim(), url: newWebhookUrl.trim(), eventType: newWebhookEvent },
|
|
53
|
+
];
|
|
54
|
+
newWebhookName = '';
|
|
55
|
+
newWebhookUrl = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deleteWebhook(id: string) {
|
|
59
|
+
webhooks = webhooks.filter(w => w.id !== id);
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function generateDemoToken() {
|
|
34
63
|
const randomHex = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
35
64
|
return `sv_demo_${randomHex}`;
|
|
@@ -190,5 +219,61 @@
|
|
|
190
219
|
</div>
|
|
191
220
|
</Card.CardContent>
|
|
192
221
|
</Card.Card>
|
|
222
|
+
|
|
223
|
+
<!-- Webhooks -->
|
|
224
|
+
<Card.Card>
|
|
225
|
+
<Card.CardHeader>
|
|
226
|
+
<Card.CardTitle class="flex items-center gap-2">
|
|
227
|
+
<Webhook class="h-4 w-4 text-muted-foreground" />
|
|
228
|
+
{i18n.t('api.webhooks')}
|
|
229
|
+
</Card.CardTitle>
|
|
230
|
+
<Card.CardDescription>{i18n.t('api.webhooksDesc')}</Card.CardDescription>
|
|
231
|
+
</Card.CardHeader>
|
|
232
|
+
<Card.CardContent class="space-y-4">
|
|
233
|
+
<form class="grid gap-3 sm:grid-cols-[1fr_1fr_180px_auto]" onsubmit={handleAddWebhook}>
|
|
234
|
+
<div class="space-y-1.5">
|
|
235
|
+
<Label for="webhook-name">{i18n.t('api.webhookName')}</Label>
|
|
236
|
+
<Input id="webhook-name" bind:value={newWebhookName} placeholder="Order Events" />
|
|
237
|
+
</div>
|
|
238
|
+
<div class="space-y-1.5">
|
|
239
|
+
<Label for="webhook-url">{i18n.t('api.webhookUrl')}</Label>
|
|
240
|
+
<Input id="webhook-url" type="url" bind:value={newWebhookUrl} placeholder="https://api.example.com/hooks" />
|
|
241
|
+
</div>
|
|
242
|
+
<div class="space-y-1.5">
|
|
243
|
+
<Label for="webhook-event">{i18n.t('api.eventType')}</Label>
|
|
244
|
+
<select id="webhook-event" bind:value={newWebhookEvent} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring">
|
|
245
|
+
<option value="resource.created">resource.created</option>
|
|
246
|
+
<option value="resource.updated">resource.updated</option>
|
|
247
|
+
<option value="resource.deleted">resource.deleted</option>
|
|
248
|
+
<option value="auth.login">auth.login</option>
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="flex items-end">
|
|
252
|
+
<Button type="submit" size="sm" disabled={!newWebhookName.trim() || !newWebhookUrl.trim()}>
|
|
253
|
+
<Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('api.addWebhook')}
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
</form>
|
|
257
|
+
|
|
258
|
+
<div class="divide-y border-t">
|
|
259
|
+
{#each webhooks as hook (hook.id)}
|
|
260
|
+
<div class="flex items-center justify-between gap-4 py-3">
|
|
261
|
+
<div class="min-w-0">
|
|
262
|
+
<p class="truncate text-sm font-medium text-foreground">{hook.name}</p>
|
|
263
|
+
<p class="truncate font-mono text-xs text-muted-foreground">{hook.url}</p>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
266
|
+
<Badge variant="secondary" class="px-2 py-0 text-[10px]">{hook.eventType}</Badge>
|
|
267
|
+
<Button variant="ghost" size="icon-sm" onclick={() => deleteWebhook(hook.id)} title={i18n.t('common.delete')}>
|
|
268
|
+
<Trash2 class="h-4 w-4" />
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
{:else}
|
|
273
|
+
<p class="py-4 text-center text-sm text-muted-foreground">{i18n.t('api.noWebhooks')}</p>
|
|
274
|
+
{/each}
|
|
275
|
+
</div>
|
|
276
|
+
</Card.CardContent>
|
|
277
|
+
</Card.Card>
|
|
193
278
|
</div>
|
|
194
279
|
</div>
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
4
|
import {
|
|
5
5
|
createTable,
|
|
6
|
-
|
|
6
|
+
tableFeatures,
|
|
7
|
+
columnOrderingFeature,
|
|
8
|
+
columnSizingFeature,
|
|
9
|
+
columnVisibilityFeature,
|
|
10
|
+
rowExpandingFeature,
|
|
11
|
+
rowSelectionFeature,
|
|
12
|
+
rowSortingFeature,
|
|
7
13
|
type ColumnDef,
|
|
8
14
|
type SortingState,
|
|
9
15
|
type RowSelectionState,
|
|
@@ -25,7 +31,6 @@
|
|
|
25
31
|
row_getIsSelected,
|
|
26
32
|
row_toggleSelected,
|
|
27
33
|
row_getVisibleCells,
|
|
28
|
-
row_toggleExpanded,
|
|
29
34
|
cell_getValue,
|
|
30
35
|
} from '@tanstack/table-core/static-functions';
|
|
31
36
|
|
|
@@ -289,6 +294,14 @@
|
|
|
289
294
|
const rowSelectionAtom = createAtom({} as RowSelectionState);
|
|
290
295
|
const expandedAtom = createAtom({} as ExpandedState);
|
|
291
296
|
const columnOrderAtom = createAtom([] as string[]);
|
|
297
|
+
const features = tableFeatures({
|
|
298
|
+
columnOrderingFeature,
|
|
299
|
+
columnSizingFeature,
|
|
300
|
+
columnVisibilityFeature,
|
|
301
|
+
rowExpandingFeature,
|
|
302
|
+
rowSelectionFeature,
|
|
303
|
+
rowSortingFeature,
|
|
304
|
+
});
|
|
292
305
|
|
|
293
306
|
const tableSorting = useSelector(sortingAtom);
|
|
294
307
|
const tableColumnVisibility = useSelector(columnVisibilityAtom);
|
|
@@ -388,12 +401,11 @@
|
|
|
388
401
|
});
|
|
389
402
|
|
|
390
403
|
// ─── Create TanStack Table ────────────────────────────────────
|
|
391
|
-
const tbl = createTable(
|
|
404
|
+
const tbl = createTable<TableFeatures, BaseRecord>(
|
|
392
405
|
{
|
|
406
|
+
features,
|
|
393
407
|
get data() { return query.data?.data ?? []; },
|
|
394
408
|
get columns() { return orderedColumns; },
|
|
395
|
-
getCoreRowModel: createCoreRowModel(),
|
|
396
|
-
manualPagination: true,
|
|
397
409
|
manualSorting: true,
|
|
398
410
|
getRowId: (row: BaseRecord) => String(row[primaryKey]),
|
|
399
411
|
atoms: {
|
|
@@ -402,16 +414,11 @@
|
|
|
402
414
|
rowSelection: rowSelectionAtom,
|
|
403
415
|
expanded: expandedAtom,
|
|
404
416
|
},
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
onRowSelectionChange: rowSelectionAtom.set,
|
|
408
|
-
onExpandedChange: expandedAtom.set,
|
|
409
|
-
get enableRowSelection() { return selectable && (canDelete || batchActions); },
|
|
417
|
+
autoResetExpanded: false,
|
|
418
|
+
get enableRowSelection() { return selectable && (canDelete || !!batchActions); },
|
|
410
419
|
get enableExpanding() { return !!expandedRowRender; },
|
|
411
420
|
getRowCanExpand: () => !!expandedRowRender,
|
|
412
421
|
},
|
|
413
|
-
// AutoTable reads focused atoms directly; selecting the full state triggers Svelte proxy equality warnings.
|
|
414
|
-
() => undefined,
|
|
415
422
|
);
|
|
416
423
|
|
|
417
424
|
$effect.pre(() => {
|
|
@@ -433,6 +440,26 @@
|
|
|
433
440
|
rows: table_getRowModel(tbl).rows,
|
|
434
441
|
};
|
|
435
442
|
});
|
|
443
|
+
|
|
444
|
+
function toggleRowExpanded(rowId: string): void {
|
|
445
|
+
const current = tableExpanded.current;
|
|
446
|
+
if (current === true) {
|
|
447
|
+
expandedAtom.set(Object.fromEntries(
|
|
448
|
+
table_getRowModel(tbl).flatRows
|
|
449
|
+
.filter((row) => row.id !== rowId)
|
|
450
|
+
.map((row) => [row.id, true])
|
|
451
|
+
));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (current[rowId]) {
|
|
455
|
+
expandedAtom.set(Object.fromEntries(
|
|
456
|
+
Object.entries(current).filter(([id, isExpanded]) => id !== rowId && isExpanded)
|
|
457
|
+
));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
expandedAtom.set({ ...current, [rowId]: true });
|
|
461
|
+
}
|
|
462
|
+
|
|
436
463
|
const totalPages = $derived(Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)));
|
|
437
464
|
|
|
438
465
|
// ─── Pagination helpers ───────────────────────────────────────
|
|
@@ -729,7 +756,7 @@
|
|
|
729
756
|
onCheckedChange={() => row_toggleSelected(row)}
|
|
730
757
|
/>
|
|
731
758
|
{:else if cell.column.id === '_expand'}
|
|
732
|
-
<TooltipButton tooltip={rowIsExpanded(row.id) ? i18n.t('common.collapse') : i18n.t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() =>
|
|
759
|
+
<TooltipButton tooltip={rowIsExpanded(row.id) ? i18n.t('common.collapse') : i18n.t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => toggleRowExpanded(row.id)}>
|
|
733
760
|
{#if rowIsExpanded(row.id)}
|
|
734
761
|
<ChevronUp class="h-4 w-4" />
|
|
735
762
|
{:else}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext, getResources } from '@svadmin/core';
|
|
2
|
+
import { captureAdminContext, getResources, type MenuItem } from '@svadmin/core';
|
|
3
3
|
import { getPath } from '../router-state.svelte.js';
|
|
4
4
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
5
5
|
|
|
@@ -10,12 +10,43 @@
|
|
|
10
10
|
const adminContext = captureAdminContext();
|
|
11
11
|
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
let { menu }: { menu?: MenuItem[] } = $props();
|
|
14
|
+
|
|
15
|
+
interface Crumb { label: string; href?: string; }
|
|
16
|
+
|
|
17
|
+
function normalizePath(path: string): string {
|
|
18
|
+
const normalized = path.replace(/^#/, '').split(/[?#]/)[0].replace(/\/$/, '');
|
|
19
|
+
return normalized || '/';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findMenuTrail(items: MenuItem[], path: string, ancestors: MenuItem[] = []): MenuItem[] | undefined {
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
const trail = [...ancestors, item];
|
|
25
|
+
if (item.href && normalizePath(item.href) === path) return trail;
|
|
26
|
+
if (item.children) {
|
|
27
|
+
const match = findMenuTrail(item.children, path, trail);
|
|
28
|
+
if (match) return match;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
14
32
|
|
|
15
33
|
const crumbs = $derived.by(() => {
|
|
16
34
|
const result: Crumb[] = [{ label: i18n.t('common.home') ?? 'Home', href: adminContext.formatLink('/') }];
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
35
|
+
const currentPathname = normalizePath(getPath());
|
|
36
|
+
if (currentPathname === '/') return result;
|
|
37
|
+
|
|
38
|
+
const menuTrail = menu ? findMenuTrail(menu, currentPathname) : undefined;
|
|
39
|
+
if (menuTrail) {
|
|
40
|
+
for (const item of menuTrail) {
|
|
41
|
+
result.push({
|
|
42
|
+
label: item.label ?? item.name,
|
|
43
|
+
href: item.href ? adminContext.formatLink(normalizePath(item.href)) : undefined,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const path = currentPathname.replace(/^\//, '');
|
|
19
50
|
|
|
20
51
|
const segments = path.split('/').filter(Boolean);
|
|
21
52
|
const resourceNames = resources.map(r => r.name);
|
|
@@ -54,7 +85,7 @@
|
|
|
54
85
|
{#if crumbs.length > 1}
|
|
55
86
|
<Breadcrumb.Root class="mb-4">
|
|
56
87
|
<Breadcrumb.List>
|
|
57
|
-
{#each crumbs as crumb, i (crumb.
|
|
88
|
+
{#each crumbs as crumb, i (`${crumb.label}-${i}`)}
|
|
58
89
|
{#if i > 0}
|
|
59
90
|
<Breadcrumb.Separator />
|
|
60
91
|
{/if}
|
|
@@ -62,8 +93,10 @@
|
|
|
62
93
|
<span class="inline-flex svadmin-page-enter" style="animation-duration: 0.2s;">
|
|
63
94
|
{#if i === crumbs.length - 1}
|
|
64
95
|
<Breadcrumb.Page>{crumb.label}</Breadcrumb.Page>
|
|
65
|
-
{:else}
|
|
96
|
+
{:else if crumb.href}
|
|
66
97
|
<Breadcrumb.Link href={crumb.href}>{crumb.label}</Breadcrumb.Link>
|
|
98
|
+
{:else}
|
|
99
|
+
<span class="text-muted-foreground">{crumb.label}</span>
|
|
67
100
|
{/if}
|
|
68
101
|
</span>
|
|
69
102
|
</Breadcrumb.Item>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { type MenuItem } from '@svadmin/core';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
menu?: MenuItem[];
|
|
4
|
+
};
|
|
5
|
+
declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
6
|
type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
|
|
3
7
|
export default Breadcrumbs;
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
const i18n = useTranslation();
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
|
+
/** Render the launcher inside a layout toolbar instead of as a floating action button. */
|
|
17
|
+
docked?: boolean;
|
|
16
18
|
/** localStorage key for chat history persistence. Set to '' to disable. */
|
|
17
19
|
persistKey?: string;
|
|
18
20
|
/** Custom persistence callback — called when messages change */
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const {
|
|
29
|
+
docked = false,
|
|
27
30
|
persistKey = 'svadmin-chat',
|
|
28
31
|
onPersist,
|
|
29
32
|
onRestore,
|
|
@@ -420,7 +423,7 @@
|
|
|
420
423
|
tooltip={i18n.t('chat.title') || 'AI Assistant'}
|
|
421
424
|
variant="default"
|
|
422
425
|
size="icon"
|
|
423
|
-
class="
|
|
426
|
+
class="{docked ? 'relative h-9 w-9 rounded-md shadow-sm' : 'fixed bottom-4 right-4 h-12 w-12 rounded-full shadow-xl hover:shadow-2xl hover:scale-110 sm:bottom-6 sm:right-6'} z-[9998] transition-all bg-primary text-primary-foreground"
|
|
424
427
|
onclick={() => { open = true; minimized = false; }}
|
|
425
428
|
>
|
|
426
429
|
<MessageCircle class="h-5 w-5" />
|
|
@@ -608,4 +611,3 @@
|
|
|
608
611
|
{/if}
|
|
609
612
|
{/if}
|
|
610
613
|
|
|
611
|
-
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ChatMessage, ChatAction } from '@svadmin/core';
|
|
2
2
|
interface Props {
|
|
3
|
+
/** Render the launcher inside a layout toolbar instead of as a floating action button. */
|
|
4
|
+
docked?: boolean;
|
|
3
5
|
/** localStorage key for chat history persistence. Set to '' to disable. */
|
|
4
6
|
persistKey?: string;
|
|
5
7
|
/** Custom persistence callback — called when messages change */
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
const i18n = useTranslation();
|
|
16
16
|
|
|
17
|
+
let { docked = false }: { docked?: boolean } = $props();
|
|
18
|
+
|
|
17
19
|
const adminContext = captureAdminContext();
|
|
18
20
|
let visible = $state(false);
|
|
19
21
|
let collapsed = $state(false);
|
|
@@ -138,7 +140,7 @@
|
|
|
138
140
|
tooltip={i18n.t('devtools.title')}
|
|
139
141
|
variant="default"
|
|
140
142
|
size="icon"
|
|
141
|
-
class="fixed bottom-
|
|
143
|
+
class="{docked ? 'relative rounded-md shadow-sm' : 'fixed bottom-5 right-[4.75rem] rounded-full shadow-lg sm:bottom-7 sm:right-[5.5rem]'} z-[9999] h-9 w-9 opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
|
142
144
|
onclick={toggle}
|
|
143
145
|
>
|
|
144
146
|
<Bug class="h-4 w-4" />
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
docked?: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare const DevTools: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type DevTools = ReturnType<typeof DevTools>;
|
|
3
6
|
export default DevTools;
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
7
7
|
import { Label } from './ui/label/index.js';
|
|
8
8
|
import * as Alert from './ui/alert/index.js';
|
|
9
|
-
import
|
|
9
|
+
import SvadminLogo from './SvadminLogo.svelte';
|
|
10
|
+
import { User, ArrowLeft, CheckCircle, Loader2, AlertCircle } from '@lucide/svelte';
|
|
10
11
|
|
|
11
12
|
const i18n = useTranslation();
|
|
12
13
|
|
|
@@ -49,9 +50,7 @@
|
|
|
49
50
|
|
|
50
51
|
<div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
|
|
51
52
|
<div class="flex items-center gap-3">
|
|
52
|
-
<
|
|
53
|
-
<Shield class="h-5 w-5 text-white" />
|
|
54
|
-
</div>
|
|
53
|
+
<SvadminLogo size={36} onDark />
|
|
55
54
|
<span class="text-lg font-semibold text-white tracking-tight">{title}</span>
|
|
56
55
|
</div>
|
|
57
56
|
|
|
@@ -75,10 +74,8 @@
|
|
|
75
74
|
<!-- Right form panel -->
|
|
76
75
|
<div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
|
|
77
76
|
<div class="w-full max-w-[400px]">
|
|
78
|
-
<div class="flex items-center gap-3
|
|
79
|
-
<
|
|
80
|
-
<Shield class="h-5 w-5" />
|
|
81
|
-
</div>
|
|
77
|
+
<div class="mb-8 flex items-center gap-3 lg:hidden">
|
|
78
|
+
<SvadminLogo size={36} />
|
|
82
79
|
<span class="text-lg font-semibold tracking-tight">{title}</span>
|
|
83
80
|
</div>
|
|
84
81
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Moon, Sun, Search, MonitorUp } from '@lucide/svelte';
|
|
4
4
|
import { Button } from './ui/button';
|
|
5
5
|
import TooltipButton from './TooltipButton.svelte';
|
|
6
|
-
import { getResolvedTheme, toggleTheme } from '@svadmin/core';
|
|
6
|
+
import { getResolvedTheme, toggleTheme, type MenuItem } from '@svadmin/core';
|
|
7
7
|
import Breadcrumbs from './Breadcrumbs.svelte';
|
|
8
8
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
9
9
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
/** Slot for additional actions on the right side of the header */
|
|
21
21
|
rightActions,
|
|
22
22
|
siteUrl,
|
|
23
|
+
menu,
|
|
23
24
|
} = $props<{
|
|
24
25
|
showThemeToggle?: boolean;
|
|
25
26
|
showBreadcrumbs?: boolean;
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
children?: import('svelte').Snippet;
|
|
29
30
|
rightActions?: import('svelte').Snippet;
|
|
30
31
|
siteUrl?: string;
|
|
32
|
+
menu?: MenuItem[];
|
|
31
33
|
}>();
|
|
32
34
|
|
|
33
35
|
// Retrieve optional component overrides from registry
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
{#if CustomBreadcrumbs}
|
|
48
50
|
<CustomBreadcrumbs />
|
|
49
51
|
{:else}
|
|
50
|
-
<Breadcrumbs />
|
|
52
|
+
<Breadcrumbs {menu} />
|
|
51
53
|
{/if}
|
|
52
54
|
{/if}
|
|
53
55
|
</div>
|
|
@@ -69,7 +71,7 @@
|
|
|
69
71
|
{#if showSearch && onSearchClick}
|
|
70
72
|
<Button variant="outline" size="sm" onclick={onSearchClick} class="gap-2 text-muted-foreground h-9 px-3 rounded-lg border-border/60">
|
|
71
73
|
<Search class="h-3.5 w-3.5" />
|
|
72
|
-
<span class="hidden sm:inline text-xs">{i18n.t('common.search')}
|
|
74
|
+
<span class="hidden sm:inline text-xs">{i18n.t('common.search')}</span>
|
|
73
75
|
<kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium font-mono text-muted-foreground">
|
|
74
76
|
<span class="text-xs">⌘</span>K
|
|
75
77
|
</kbd>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MenuItem } from '@svadmin/core';
|
|
1
2
|
type $$ComponentProps = {
|
|
2
3
|
showThemeToggle?: boolean;
|
|
3
4
|
showBreadcrumbs?: boolean;
|
|
@@ -6,6 +7,7 @@ type $$ComponentProps = {
|
|
|
6
7
|
children?: import('svelte').Snippet;
|
|
7
8
|
rightActions?: import('svelte').Snippet;
|
|
8
9
|
siteUrl?: string;
|
|
10
|
+
menu?: MenuItem[];
|
|
9
11
|
};
|
|
10
12
|
declare const Header: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
13
|
type Header = ReturnType<typeof Header>;
|