@svadmin/ui 0.0.6 → 0.4.1
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/package.json +8 -3
- package/src/actions.ts +110 -0
- package/src/components/AdminApp.svelte +8 -4
- package/src/components/Authenticated.svelte +38 -13
- package/src/components/AutoForm.svelte +163 -74
- package/src/components/AutoSaveIndicator.svelte +35 -0
- package/src/components/AutoTable.svelte +476 -183
- package/src/components/Breadcrumbs.svelte +22 -17
- package/src/components/CatchAllNavigate.svelte +21 -0
- package/src/components/ComboboxField.svelte +122 -0
- package/src/components/CommandPalette.svelte +93 -0
- package/src/components/ConfigErrorScreen.svelte +53 -151
- package/src/components/ConfirmDialog.svelte +25 -17
- package/src/components/CreatePage.svelte +43 -0
- package/src/components/DevTools.svelte +99 -230
- package/src/components/DrawerForm.svelte +19 -10
- package/src/components/EditPage.svelte +71 -0
- package/src/components/EmptyState.svelte +21 -31
- package/src/components/ErrorBoundary.svelte +38 -40
- package/src/components/ErrorComponent.svelte +36 -0
- package/src/components/FieldRenderer.svelte +240 -75
- package/src/components/ForgotPasswordPage.svelte +20 -111
- package/src/components/Header.svelte +46 -57
- package/src/components/InferencerPanel.svelte +136 -110
- package/src/components/InfiniteList.svelte +93 -0
- package/src/components/Layout.svelte +85 -22
- package/src/components/ListPage.svelte +71 -0
- package/src/components/LiveIndicator.svelte +58 -0
- package/src/components/LoginPage.svelte +33 -154
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/NavigateToResource.svelte +20 -0
- package/src/components/PageSkeleton.svelte +71 -0
- package/src/components/PasswordInput.svelte +95 -0
- package/src/components/RegisterPage.svelte +35 -164
- package/src/components/ShowPage.svelte +35 -36
- package/src/components/Sidebar.svelte +212 -81
- package/src/components/StatsCard.svelte +12 -3
- package/src/components/StepsForm.svelte +139 -0
- package/src/components/ThemedTitle.svelte +32 -0
- package/src/components/Toast.svelte +15 -44
- package/src/components/TooltipButton.svelte +52 -0
- package/src/components/UndoableNotification.svelte +28 -96
- package/src/components/UpdatePasswordPage.svelte +32 -160
- package/src/components/buttons/CloneButton.svelte +1 -1
- package/src/components/buttons/CreateButton.svelte +1 -1
- package/src/components/buttons/DeleteButton.svelte +3 -4
- package/src/components/buttons/EditButton.svelte +1 -1
- package/src/components/buttons/ImportButton.svelte +29 -7
- package/src/components/buttons/RefreshButton.svelte +11 -5
- package/src/components/fieldComponentMap.ts +79 -0
- package/src/components/fields/BooleanField.svelte +13 -0
- package/src/components/fields/DateField.svelte +6 -0
- package/src/components/fields/EmailField.svelte +9 -0
- package/src/components/fields/FileField.svelte +14 -0
- package/src/components/fields/ImageField.svelte +9 -0
- package/src/components/fields/JsonField.svelte +72 -0
- package/src/components/fields/MarkdownField.svelte +9 -0
- package/src/components/fields/MultiSelectField.svelte +20 -0
- package/src/components/fields/NumberField.svelte +6 -0
- package/src/components/fields/RelationField.svelte +24 -0
- package/src/components/fields/RichTextField.svelte +35 -0
- package/src/components/fields/SelectField.svelte +14 -0
- package/src/components/fields/TagField.svelte +12 -0
- package/src/components/fields/TextField.svelte +11 -0
- package/src/components/fields/UrlField.svelte +14 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
- package/src/components/ui/alert-dialog/index.ts +20 -0
- package/src/components/ui/avatar/avatar.svelte +2 -1
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
- package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/collapsible/index.ts +7 -0
- package/src/components/ui/command/command-dialog.svelte +31 -0
- package/src/components/ui/command/index.ts +2 -0
- package/src/components/ui/context-menu/index.ts +11 -0
- package/src/components/ui/drawer/drawer-content.svelte +21 -0
- package/src/components/ui/drawer/drawer-description.svelte +11 -0
- package/src/components/ui/drawer/drawer-footer.svelte +11 -0
- package/src/components/ui/drawer/drawer-header.svelte +11 -0
- package/src/components/ui/drawer/drawer-title.svelte +11 -0
- package/src/components/ui/drawer/drawer.svelte +18 -0
- package/src/components/ui/drawer/index.ts +6 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
- package/src/components/ui/dropdown-menu/index.ts +6 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/label/label.svelte +18 -0
- package/src/components/ui/pagination/index.ts +7 -0
- package/src/components/ui/pagination/pagination-content.svelte +11 -0
- package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
- package/src/components/ui/pagination/pagination-item.svelte +11 -0
- package/src/components/ui/pagination/pagination-link.svelte +26 -0
- package/src/components/ui/pagination/pagination-next.svelte +22 -0
- package/src/components/ui/pagination/pagination-previous.svelte +22 -0
- package/src/components/ui/pagination/pagination.svelte +11 -0
- package/src/components/ui/popover/index.ts +12 -0
- package/src/components/ui/popover/popover-content.svelte +25 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/progress/progress.svelte +22 -0
- package/src/components/ui/scroll-area/index.ts +1 -0
- package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
- package/src/components/ui/sheet/index.ts +15 -1
- package/src/components/ui/sheet/sheet-content.svelte +28 -0
- package/src/components/ui/sheet/sheet-description.svelte +22 -0
- package/src/components/ui/sheet/sheet-footer.svelte +22 -0
- package/src/components/ui/sheet/sheet-header.svelte +22 -0
- package/src/components/ui/sheet/sheet-title.svelte +22 -0
- package/src/index.ts +52 -1
- package/src/router-state.svelte.ts +16 -9
|
@@ -1,66 +1,55 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import { Moon, Sun, Search } from 'lucide-svelte';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
5
|
+
import { getResolvedTheme, toggleTheme } from '@svadmin/core';
|
|
6
|
+
import Breadcrumbs from './Breadcrumbs.svelte';
|
|
5
7
|
import { t } from '@svadmin/core/i18n';
|
|
6
|
-
import { Button } from './ui/button/index.js';
|
|
7
|
-
import { Sun, Moon, Bell, Search } from 'lucide-svelte';
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
identity?: Identity | null;
|
|
11
|
-
title?: string;
|
|
12
|
-
showSearch?: boolean;
|
|
13
|
-
actions?: Snippet;
|
|
14
|
-
class?: string;
|
|
15
|
-
}
|
|
16
8
|
|
|
17
9
|
let {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
showSearch =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
10
|
+
showThemeToggle = true,
|
|
11
|
+
showBreadcrumbs = true,
|
|
12
|
+
showSearch = true,
|
|
13
|
+
onSearchClick,
|
|
14
|
+
children,
|
|
15
|
+
} = $props<{
|
|
16
|
+
showThemeToggle?: boolean;
|
|
17
|
+
showBreadcrumbs?: boolean;
|
|
18
|
+
showSearch?: boolean;
|
|
19
|
+
onSearchClick?: () => void;
|
|
20
|
+
children?: import('svelte').Snippet;
|
|
21
|
+
}>();
|
|
24
22
|
</script>
|
|
25
23
|
|
|
26
|
-
<header class="sticky top-0 z-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<Search class="absolute left-2.5 top-2 h-4 w-4 text-muted-foreground" />
|
|
36
|
-
<input
|
|
37
|
-
type="search"
|
|
38
|
-
placeholder={t('common.search')}
|
|
39
|
-
class="h-8 w-64 rounded-lg border border-input bg-background pl-8 pr-3 text-sm outline-none focus:border-ring focus:ring-1 focus:ring-ring"
|
|
40
|
-
/>
|
|
41
|
-
</div>
|
|
42
|
-
{/if}
|
|
43
|
-
|
|
44
|
-
{#if actions}
|
|
45
|
-
{@render actions()}
|
|
46
|
-
{/if}
|
|
47
|
-
|
|
48
|
-
<!-- Theme toggle -->
|
|
49
|
-
<Button variant="ghost" size="icon-sm" onclick={toggleTheme} title={t('common.toggleTheme')}>
|
|
50
|
-
{#if getResolvedTheme() === 'dark'}
|
|
51
|
-
<Sun class="h-4 w-4" />
|
|
52
|
-
{:else}
|
|
53
|
-
<Moon class="h-4 w-4" />
|
|
24
|
+
<header class="sticky top-0 z-30 flex h-16 w-full shrink-0 items-center justify-between border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-6">
|
|
25
|
+
<div class="flex items-center gap-4">
|
|
26
|
+
{#if showBreadcrumbs}
|
|
27
|
+
<Breadcrumbs />
|
|
28
|
+
{/if}
|
|
29
|
+
</div>
|
|
30
|
+
<div class="ml-auto flex items-center gap-2">
|
|
31
|
+
{#if children}
|
|
32
|
+
{@render children()}
|
|
54
33
|
{/if}
|
|
55
|
-
</Button>
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
35
|
+
{#if showSearch && onSearchClick}
|
|
36
|
+
<Button variant="outline" size="sm" onclick={onSearchClick} class="gap-2 text-muted-foreground">
|
|
37
|
+
<Search class="h-4 w-4" />
|
|
38
|
+
<span class="hidden sm:inline text-xs">Search...</span>
|
|
39
|
+
<kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
40
|
+
<span class="text-xs">⌘</span>K
|
|
41
|
+
</kbd>
|
|
42
|
+
</Button>
|
|
43
|
+
{/if}
|
|
44
|
+
|
|
45
|
+
{#if showThemeToggle}
|
|
46
|
+
<TooltipButton tooltip={t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-full">
|
|
47
|
+
{#if getResolvedTheme() === 'dark'}
|
|
48
|
+
<Moon class="h-5 w-5 transition-all" />
|
|
49
|
+
{:else}
|
|
50
|
+
<Sun class="h-5 w-5 transition-all" />
|
|
51
|
+
{/if}
|
|
52
|
+
</TooltipButton>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
66
55
|
</header>
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getDataProvider, getResources, inferResource } from '@svadmin/core';
|
|
3
|
-
import type { InferResult
|
|
3
|
+
import type { InferResult } from '@svadmin/core';
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Input } from './ui/input/index.js';
|
|
6
|
+
import { Badge } from './ui/badge/index.js';
|
|
7
|
+
import * as Card from './ui/card/index.js';
|
|
8
|
+
import * as Table from './ui/table/index.js';
|
|
9
|
+
import * as Alert from './ui/alert/index.js';
|
|
10
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
11
|
+
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
12
|
+
import { Select } from './ui/select/index.js';
|
|
13
|
+
import { Wand2, Copy, Check, RefreshCw, Loader2, AlertCircle } from 'lucide-svelte';
|
|
6
14
|
|
|
7
15
|
const dataProvider = getDataProvider();
|
|
8
16
|
const resources = getResources();
|
|
@@ -13,6 +21,11 @@
|
|
|
13
21
|
let error = $state<string | null>(null);
|
|
14
22
|
let copied = $state(false);
|
|
15
23
|
let customEndpoint = $state('');
|
|
24
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
return () => { if (copyTimer) clearTimeout(copyTimer); };
|
|
28
|
+
});
|
|
16
29
|
|
|
17
30
|
async function runInference() {
|
|
18
31
|
const resourceName = customEndpoint.trim() || selectedResource;
|
|
@@ -37,8 +50,8 @@
|
|
|
37
50
|
resourceName,
|
|
38
51
|
response.data as Record<string, unknown>[],
|
|
39
52
|
);
|
|
40
|
-
} catch (e:
|
|
41
|
-
error = e
|
|
53
|
+
} catch (e: unknown) {
|
|
54
|
+
error = e instanceof Error ? e.message : 'Failed to fetch data for inference.';
|
|
42
55
|
} finally {
|
|
43
56
|
loading = false;
|
|
44
57
|
}
|
|
@@ -48,119 +61,132 @@
|
|
|
48
61
|
if (!inferResult) return;
|
|
49
62
|
navigator.clipboard.writeText(inferResult.code);
|
|
50
63
|
copied = true;
|
|
51
|
-
|
|
64
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
65
|
+
copyTimer = setTimeout(() => { copied = false; }, 2000);
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
const
|
|
55
|
-
text: '
|
|
56
|
-
number: '
|
|
57
|
-
boolean: '
|
|
58
|
-
date: '
|
|
59
|
-
email: '
|
|
60
|
-
url: '
|
|
61
|
-
image: '
|
|
62
|
-
images: '
|
|
63
|
-
textarea: '
|
|
64
|
-
json: '
|
|
65
|
-
tags: '
|
|
66
|
-
select: '
|
|
67
|
-
relation: '
|
|
68
|
-
color: '
|
|
69
|
-
phone: '
|
|
68
|
+
const typeVariants: Record<string, 'default' | 'secondary' | 'outline' | 'destructive'> = {
|
|
69
|
+
text: 'secondary',
|
|
70
|
+
number: 'default',
|
|
71
|
+
boolean: 'outline',
|
|
72
|
+
date: 'secondary',
|
|
73
|
+
email: 'default',
|
|
74
|
+
url: 'outline',
|
|
75
|
+
image: 'secondary',
|
|
76
|
+
images: 'secondary',
|
|
77
|
+
textarea: 'outline',
|
|
78
|
+
json: 'secondary',
|
|
79
|
+
tags: 'default',
|
|
80
|
+
select: 'outline',
|
|
81
|
+
relation: 'destructive',
|
|
82
|
+
color: 'secondary',
|
|
83
|
+
phone: 'default',
|
|
70
84
|
};
|
|
71
85
|
</script>
|
|
72
86
|
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
87
|
+
<Card.Root>
|
|
88
|
+
<Card.CardHeader class="pb-3">
|
|
89
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
90
|
+
<Wand2 class="h-4 w-4" />
|
|
91
|
+
Resource Inferencer
|
|
92
|
+
</Card.CardTitle>
|
|
93
|
+
</Card.CardHeader>
|
|
94
|
+
<Card.CardContent class="space-y-4">
|
|
95
|
+
<!-- Resource selector -->
|
|
96
|
+
<div class="flex gap-2">
|
|
97
|
+
<Select
|
|
98
|
+
class="flex-1"
|
|
99
|
+
bind:value={selectedResource}
|
|
100
|
+
placeholder="— Select a resource —"
|
|
101
|
+
>
|
|
102
|
+
{#each resources as res}
|
|
103
|
+
<option value={res.name}>{res.label} ({res.name})</option>
|
|
104
|
+
{/each}
|
|
105
|
+
</Select>
|
|
106
|
+
<span class="flex items-center text-xs text-muted-foreground">or</span>
|
|
107
|
+
<Input
|
|
108
|
+
type="text"
|
|
109
|
+
class="w-36"
|
|
110
|
+
placeholder="custom endpoint"
|
|
111
|
+
bind:value={customEndpoint}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<Button size="sm" onclick={runInference} disabled={loading || (!selectedResource && !customEndpoint.trim())}>
|
|
116
|
+
{#if loading}
|
|
117
|
+
<Loader2 class="h-3 w-3 animate-spin" data-icon="inline-start" />
|
|
118
|
+
Inferring...
|
|
119
|
+
{:else}
|
|
120
|
+
<RefreshCw class="h-3 w-3" data-icon="inline-start" />
|
|
121
|
+
Infer Fields
|
|
122
|
+
{/if}
|
|
123
|
+
</Button>
|
|
124
|
+
|
|
125
|
+
{#if error}
|
|
126
|
+
<Alert.Root variant="destructive">
|
|
127
|
+
<AlertCircle class="h-4 w-4" />
|
|
128
|
+
<Alert.Description>{error}</Alert.Description>
|
|
129
|
+
</Alert.Root>
|
|
106
130
|
{/if}
|
|
107
|
-
</Button>
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<!-- Field table -->
|
|
117
|
-
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
|
118
|
-
<table class="w-full text-left text-sm">
|
|
119
|
-
<thead class="bg-gray-50 dark:bg-gray-800">
|
|
120
|
-
<tr>
|
|
121
|
-
<th class="px-3 py-2 font-medium">Field</th>
|
|
122
|
-
<th class="px-3 py-2 font-medium">Type</th>
|
|
123
|
-
<th class="px-3 py-2 font-medium">List</th>
|
|
124
|
-
<th class="px-3 py-2 font-medium">Form</th>
|
|
125
|
-
</tr>
|
|
126
|
-
</thead>
|
|
127
|
-
<tbody>
|
|
128
|
-
{#each inferResult.fields as field}
|
|
129
|
-
<tr class="border-t border-gray-100 dark:border-gray-700/50">
|
|
130
|
-
<td class="px-3 py-1.5 font-mono text-xs">{field.key}</td>
|
|
131
|
-
<td class="px-3 py-1.5">
|
|
132
|
-
<span class="inline-block rounded-full px-2 py-0.5 text-xs font-medium {typeColors[field.type] ?? 'bg-gray-100 text-gray-600'}">
|
|
133
|
-
{field.type}
|
|
134
|
-
{#if field.resource}→ {field.resource}{/if}
|
|
135
|
-
</span>
|
|
136
|
-
</td>
|
|
137
|
-
<td class="px-3 py-1.5 text-center">{field.showInList ? '✓' : '—'}</td>
|
|
138
|
-
<td class="px-3 py-1.5 text-center">{field.showInForm ? '✓' : '—'}</td>
|
|
139
|
-
</tr>
|
|
140
|
-
{/each}
|
|
141
|
-
</tbody>
|
|
142
|
-
</table>
|
|
143
|
-
</div>
|
|
132
|
+
{#if loading}
|
|
133
|
+
<div class="space-y-2">
|
|
134
|
+
{#each Array(4) as _}
|
|
135
|
+
<Skeleton class="h-8 w-full" />
|
|
136
|
+
{/each}
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
{#if inferResult}
|
|
141
|
+
<!-- Field table -->
|
|
142
|
+
<ScrollArea class="max-h-80">
|
|
143
|
+
<Table.Root>
|
|
144
|
+
<Table.Header>
|
|
145
|
+
<Table.Row class="bg-muted/50">
|
|
146
|
+
<Table.Head>Field</Table.Head>
|
|
147
|
+
<Table.Head>Type</Table.Head>
|
|
148
|
+
<Table.Head class="text-center">List</Table.Head>
|
|
149
|
+
<Table.Head class="text-center">Form</Table.Head>
|
|
150
|
+
</Table.Row>
|
|
151
|
+
</Table.Header>
|
|
152
|
+
<Table.Body>
|
|
153
|
+
{#each inferResult.fields as field}
|
|
154
|
+
<Table.Row>
|
|
155
|
+
<Table.Cell class="font-mono text-xs">{field.key}</Table.Cell>
|
|
156
|
+
<Table.Cell>
|
|
157
|
+
<Badge variant={typeVariants[field.type] ?? 'secondary'}>
|
|
158
|
+
{field.type}
|
|
159
|
+
{#if field.resource}→ {field.resource}{/if}
|
|
160
|
+
</Badge>
|
|
161
|
+
</Table.Cell>
|
|
162
|
+
<Table.Cell class="text-center">{field.showInList ? '✓' : '—'}</Table.Cell>
|
|
163
|
+
<Table.Cell class="text-center">{field.showInForm ? '✓' : '—'}</Table.Cell>
|
|
164
|
+
</Table.Row>
|
|
165
|
+
{/each}
|
|
166
|
+
</Table.Body>
|
|
167
|
+
</Table.Root>
|
|
168
|
+
</ScrollArea>
|
|
169
|
+
|
|
170
|
+
<!-- Generated code -->
|
|
171
|
+
<div class="rounded-lg border overflow-hidden">
|
|
172
|
+
<div class="flex items-center justify-between bg-muted px-3 py-1.5 text-xs text-muted-foreground">
|
|
173
|
+
<span>Generated ResourceDefinition</span>
|
|
174
|
+
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1" onclick={copyCode}>
|
|
175
|
+
{#if copied}
|
|
176
|
+
<Check class="h-3 w-3 text-emerald-500" />
|
|
177
|
+
<span class="text-emerald-500">Copied!</span>
|
|
178
|
+
{:else}
|
|
179
|
+
<Copy class="h-3 w-3" />
|
|
180
|
+
Copy
|
|
181
|
+
{/if}
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
<pre class="max-h-64 overflow-auto bg-muted/30 p-3 text-xs text-foreground font-mono">{inferResult.code}</pre>
|
|
158
185
|
</div>
|
|
159
|
-
<pre class="max-h-64 overflow-auto rounded-b-lg bg-gray-900 p-3 text-xs text-green-300 font-mono">{inferResult.code}</pre>
|
|
160
|
-
</div>
|
|
161
186
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
</
|
|
187
|
+
<p class="text-xs text-muted-foreground">
|
|
188
|
+
Inferred {inferResult.fields.length} fields from sample data. Copy the code above into your <code class="bg-muted px-1 rounded text-foreground">resources.ts</code> file.
|
|
189
|
+
</p>
|
|
190
|
+
{/if}
|
|
191
|
+
</Card.CardContent>
|
|
192
|
+
</Card.Root>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade, fly } from 'svelte/transition';
|
|
3
|
+
import { useInfiniteList } from '@svadmin/core';
|
|
4
|
+
import type { BaseRecord, Sort, Filter } from '@svadmin/core';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import { t } from '@svadmin/core/i18n';
|
|
7
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
8
|
+
import { Button } from './ui/button/index.js';
|
|
9
|
+
import { Loader2 } from 'lucide-svelte';
|
|
10
|
+
import { intersect } from '../actions.js';
|
|
11
|
+
|
|
12
|
+
interface Props<T extends BaseRecord = BaseRecord> {
|
|
13
|
+
resource: string;
|
|
14
|
+
pageSize?: number;
|
|
15
|
+
sorters?: Sort[];
|
|
16
|
+
filters?: Filter[];
|
|
17
|
+
/** Render each item */
|
|
18
|
+
children: Snippet<[{ item: T; index: number }]>;
|
|
19
|
+
/** Custom empty state */
|
|
20
|
+
empty?: Snippet;
|
|
21
|
+
/** Custom loading skeleton */
|
|
22
|
+
loadingSkeleton?: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
resource,
|
|
27
|
+
pageSize = 20,
|
|
28
|
+
sorters,
|
|
29
|
+
filters,
|
|
30
|
+
children,
|
|
31
|
+
empty,
|
|
32
|
+
loadingSkeleton,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const { query } = useInfiniteList({
|
|
36
|
+
resource,
|
|
37
|
+
pagination: { pageSize },
|
|
38
|
+
sorters,
|
|
39
|
+
filters,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const allItems = $derived(
|
|
43
|
+
(query.data as { pages?: { data: BaseRecord[] }[] })?.pages?.flatMap(p => p.data) ?? []
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const hasNextPage = $derived(query.hasNextPage ?? false);
|
|
47
|
+
const isFetchingNextPage = $derived(query.isFetchingNextPage ?? false);
|
|
48
|
+
|
|
49
|
+
function loadMore() {
|
|
50
|
+
if (hasNextPage && !isFetchingNextPage) {
|
|
51
|
+
query.fetchNextPage();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="space-y-2">
|
|
57
|
+
{#if query.isLoading}
|
|
58
|
+
{#if loadingSkeleton}
|
|
59
|
+
{@render loadingSkeleton()}
|
|
60
|
+
{:else}
|
|
61
|
+
<div class="space-y-3">
|
|
62
|
+
{#each Array(5) as _}
|
|
63
|
+
<Skeleton class="h-16 w-full rounded-lg" />
|
|
64
|
+
{/each}
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
{:else if allItems.length === 0}
|
|
68
|
+
{#if empty}
|
|
69
|
+
{@render empty()}
|
|
70
|
+
{:else}
|
|
71
|
+
<p class="text-center text-sm text-muted-foreground py-8">{t('empty.title')}</p>
|
|
72
|
+
{/if}
|
|
73
|
+
{:else}
|
|
74
|
+
{#each allItems as item, index}
|
|
75
|
+
<div in:fade={{ duration: 200, delay: index < 20 ? index * 30 : 0 }}>
|
|
76
|
+
{@render children({ item, index })}
|
|
77
|
+
</div>
|
|
78
|
+
{/each}
|
|
79
|
+
|
|
80
|
+
{#if isFetchingNextPage}
|
|
81
|
+
<div class="flex justify-center py-4">
|
|
82
|
+
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
{#if hasNextPage}
|
|
87
|
+
<!-- Sentinel for infinite scroll -->
|
|
88
|
+
<div use:intersect={loadMore} class="h-1"></div>
|
|
89
|
+
{:else}
|
|
90
|
+
<p class="text-center text-xs text-muted-foreground py-2">{t('empty.noMore')}</p>
|
|
91
|
+
{/if}
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { fade, fly } from 'svelte/transition';
|
|
4
4
|
import Sidebar from './Sidebar.svelte';
|
|
5
5
|
import Toast from './Toast.svelte';
|
|
6
|
-
import
|
|
6
|
+
import Header from './Header.svelte';
|
|
7
|
+
import CommandPalette from './CommandPalette.svelte';
|
|
8
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
9
|
+
import { t } from '@svadmin/core/i18n';
|
|
7
10
|
import { getAuthProvider } from '@svadmin/core';
|
|
8
11
|
import type { Identity } from '@svadmin/core';
|
|
9
12
|
import { navigate } from '@svadmin/core/router';
|
|
10
|
-
import {
|
|
13
|
+
import { currentPath } from '@svadmin/core/router';
|
|
14
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
15
|
+
import { Button } from './ui/button/index.js';
|
|
16
|
+
import * as Sheet from './ui/sheet/index.js';
|
|
17
|
+
import { Menu } from 'lucide-svelte';
|
|
18
|
+
|
|
19
|
+
let commandOpen = $state(false);
|
|
20
|
+
let mobileMenuOpen = $state(false);
|
|
11
21
|
|
|
12
22
|
let { children, title = 'Admin' }: { children: Snippet; title?: string } = $props();
|
|
13
23
|
|
|
@@ -21,18 +31,23 @@
|
|
|
21
31
|
let loading = $state(true);
|
|
22
32
|
let identity = $state<Identity | null>(null);
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
$effect(() => {
|
|
25
35
|
if (!auth) {
|
|
26
36
|
loading = false;
|
|
27
37
|
return;
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const { authenticated, redirectTo } = await auth.check();
|
|
42
|
+
if (!authenticated) {
|
|
43
|
+
navigate(redirectTo ?? '/login');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
identity = await auth.getIdentity();
|
|
47
|
+
} finally {
|
|
48
|
+
loading = false;
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
36
51
|
});
|
|
37
52
|
|
|
38
53
|
async function handleLogout() {
|
|
@@ -46,21 +61,69 @@
|
|
|
46
61
|
let collapsed = $state(false);
|
|
47
62
|
</script>
|
|
48
63
|
|
|
64
|
+
<div role="application" onkeydown={(e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); commandOpen = true; } }}>
|
|
49
65
|
{#if loading}
|
|
50
|
-
<div class="flex h-screen
|
|
51
|
-
<
|
|
66
|
+
<div class="flex h-screen" in:fade={{ duration: 150 }}>
|
|
67
|
+
<div class="hidden md:block w-64 border-r bg-sidebar/80 p-4 space-y-4">
|
|
68
|
+
<Skeleton class="h-8 w-32" />
|
|
69
|
+
<div class="space-y-2 mt-6">
|
|
70
|
+
{#each Array(5) as _}
|
|
71
|
+
<Skeleton class="h-9 w-full rounded-lg" />
|
|
72
|
+
{/each}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="flex-1 p-8 space-y-6">
|
|
76
|
+
<Skeleton class="h-8 w-48" />
|
|
77
|
+
<div class="space-y-3">
|
|
78
|
+
{#each Array(4) as _}
|
|
79
|
+
<Skeleton class="h-12 w-full" />
|
|
80
|
+
{/each}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
52
83
|
</div>
|
|
53
84
|
{:else}
|
|
54
|
-
<div class="flex h-screen bg-gradient-to-br from-background via-background to-muted/30">
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
<div class="flex h-screen bg-gradient-to-br from-background via-background to-muted/30" in:fade={{ duration: 200, delay: 50 }}>
|
|
86
|
+
<!-- Desktop sidebar -->
|
|
87
|
+
<div class="hidden md:block">
|
|
88
|
+
<Sidebar {collapsed} {identity} {title} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Mobile sidebar via Sheet -->
|
|
92
|
+
<Sheet.Root bind:open={mobileMenuOpen} side="left">
|
|
93
|
+
<div class="md:hidden">
|
|
94
|
+
<Sidebar collapsed={false} {identity} {title} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
|
|
95
|
+
</div>
|
|
96
|
+
</Sheet.Root>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
class="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
|
100
|
+
class:md:ml-64={!collapsed}
|
|
101
|
+
class:md:ml-16={collapsed}
|
|
60
102
|
>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
<!-- Header with mobile hamburger -->
|
|
104
|
+
<Header
|
|
105
|
+
showSearch={true}
|
|
106
|
+
onSearchClick={() => { commandOpen = true; }}
|
|
107
|
+
>
|
|
108
|
+
{#snippet children()}
|
|
109
|
+
<!-- Mobile hamburger -->
|
|
110
|
+
<TooltipButton tooltip={t('common.menu')} class="md:hidden" onclick={() => { mobileMenuOpen = true; }}>
|
|
111
|
+
<Menu class="h-5 w-5" />
|
|
112
|
+
</TooltipButton>
|
|
113
|
+
{/snippet}
|
|
114
|
+
</Header>
|
|
115
|
+
|
|
116
|
+
<!-- Content area -->
|
|
117
|
+
<main class="flex-1 overflow-y-auto p-6 md:p-8">
|
|
118
|
+
{#key currentPath()}
|
|
119
|
+
<div in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
|
|
120
|
+
{@render children()}
|
|
121
|
+
</div>
|
|
122
|
+
{/key}
|
|
123
|
+
</main>
|
|
124
|
+
</div>
|
|
64
125
|
</div>
|
|
126
|
+
<CommandPalette bind:open={commandOpen} />
|
|
65
127
|
<Toast />
|
|
66
128
|
{/if}
|
|
129
|
+
</div>
|