@svadmin/ui 0.49.3 → 0.51.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/components/AutoForm.svelte +105 -54
- package/dist/components/AutoForm.svelte.d.ts +4 -0
- package/dist/components/AutoTable.svelte +223 -106
- package/dist/components/AutoTable.svelte.d.ts +5 -0
- package/dist/components/Breadcrumbs.svelte +3 -2
- package/dist/components/Breadcrumbs.svelte.d.ts +1 -0
- package/dist/components/CreatePage.svelte +25 -5
- package/dist/components/CreatePage.svelte.d.ts +3 -0
- package/dist/components/DrawerForm.svelte +1 -1
- package/dist/components/EditPage.svelte +25 -5
- package/dist/components/EditPage.svelte.d.ts +3 -0
- package/dist/components/FieldRenderer.svelte +15 -2
- package/dist/components/FieldRenderer.svelte.d.ts +1 -0
- package/dist/components/ListPage.svelte +29 -3
- package/dist/components/ListPage.svelte.d.ts +9 -0
- package/dist/components/ModalForm.svelte +1 -1
- package/dist/components/PageHeader.svelte +57 -10
- package/dist/components/PageHeader.svelte.d.ts +6 -0
- package/dist/components/ShowPage.svelte +97 -26
- package/dist/components/ShowPage.svelte.d.ts +10 -3
- package/dist/components/buttons/CloneButton.svelte +23 -2
- package/dist/components/buttons/CloneButton.svelte.d.ts +3 -0
- package/dist/components/buttons/CreateButton.svelte +22 -2
- package/dist/components/buttons/CreateButton.svelte.d.ts +3 -0
- package/dist/components/buttons/DeleteButton.svelte +23 -3
- package/dist/components/buttons/DeleteButton.svelte.d.ts +3 -0
- package/dist/components/buttons/EditButton.svelte +23 -2
- package/dist/components/buttons/EditButton.svelte.d.ts +3 -0
- package/dist/components/buttons/ExportButton.svelte +22 -3
- package/dist/components/buttons/ExportButton.svelte.d.ts +3 -0
- package/dist/components/buttons/ImportButton.svelte +39 -17
- package/dist/components/buttons/ImportButton.svelte.d.ts +3 -0
- package/dist/components/buttons/ListButton.svelte +29 -4
- package/dist/components/buttons/ListButton.svelte.d.ts +4 -0
- package/dist/components/buttons/RefreshButton.svelte +22 -2
- package/dist/components/buttons/RefreshButton.svelte.d.ts +3 -0
- package/dist/components/buttons/SaveButton.svelte +23 -2
- package/dist/components/buttons/SaveButton.svelte.d.ts +3 -0
- package/dist/components/buttons/ShowButton.svelte +32 -4
- package/dist/components/buttons/ShowButton.svelte.d.ts +4 -0
- package/dist/components/content/DescriptionList.svelte +125 -15
- package/dist/components/content/DescriptionList.svelte.d.ts +6 -2
- package/dist/components/content/FilterToolbar.svelte +81 -8
- package/dist/components/content/FilterToolbar.svelte.d.ts +7 -1
- package/dist/components/content/StatusTabs.svelte +156 -0
- package/dist/components/content/StatusTabs.svelte.d.ts +18 -0
- package/dist/components/content/index.d.ts +2 -0
- package/dist/components/content/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -4,6 +4,11 @@ interface Props {
|
|
|
4
4
|
resourceName: string;
|
|
5
5
|
selectable?: boolean;
|
|
6
6
|
density?: 'compact' | 'comfortable';
|
|
7
|
+
showHeader?: boolean;
|
|
8
|
+
title?: string;
|
|
9
|
+
showFilterTags?: boolean;
|
|
10
|
+
showDensitySwitcher?: boolean;
|
|
11
|
+
showRefresh?: boolean;
|
|
7
12
|
/** Custom header actions (right side) */
|
|
8
13
|
headerActions?: Snippet;
|
|
9
14
|
/** Custom cell renderers by field key */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
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
|
+
import { cn } from '../utils.js';
|
|
5
6
|
|
|
6
7
|
import * as Breadcrumb from './ui/breadcrumb/index.js';
|
|
7
8
|
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
const adminContext = captureAdminContext();
|
|
11
12
|
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
12
13
|
|
|
13
|
-
let { menu }: { menu?: MenuItem[] } = $props();
|
|
14
|
+
let { menu, class: className = 'mb-4' }: { menu?: MenuItem[]; class?: string } = $props();
|
|
14
15
|
|
|
15
16
|
interface Crumb { label: string; href?: string; }
|
|
16
17
|
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
</script>
|
|
84
85
|
|
|
85
86
|
{#if crumbs.length > 1}
|
|
86
|
-
<Breadcrumb.Root class=
|
|
87
|
+
<Breadcrumb.Root class={cn(className)}>
|
|
87
88
|
<Breadcrumb.List>
|
|
88
89
|
{#each crumbs as crumb, i (`${crumb.label}-${i}`)}
|
|
89
90
|
{#if i > 0}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getResource } from '@svadmin/core';
|
|
2
|
+
import { getResource, captureAdminContext } from '@svadmin/core';
|
|
3
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
5
5
|
import PageHeader from './PageHeader.svelte';
|
|
@@ -11,30 +11,50 @@
|
|
|
11
11
|
interface Props {
|
|
12
12
|
resourceName: string;
|
|
13
13
|
title?: string;
|
|
14
|
+
density?: 'compact' | 'comfortable';
|
|
15
|
+
columns?: 1 | 2 | 3 | 4;
|
|
14
16
|
headerActions?: Snippet;
|
|
17
|
+
onSuccess?: () => void;
|
|
15
18
|
class?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
let {
|
|
19
22
|
resourceName,
|
|
20
23
|
title,
|
|
24
|
+
density = 'comfortable',
|
|
25
|
+
columns = 1,
|
|
21
26
|
headerActions,
|
|
27
|
+
onSuccess,
|
|
22
28
|
class: className = '',
|
|
23
29
|
}: Props = $props();
|
|
30
|
+
const adminContext = captureAdminContext();
|
|
31
|
+
let navigateGuard = $state<(fn: () => void) => void>((fn) => fn());
|
|
24
32
|
|
|
25
33
|
const resource = $derived(getResource(resourceName));
|
|
26
34
|
const pageTitle = $derived(title ?? `${i18n.t('common.create')}${resource.label}`);
|
|
27
35
|
</script>
|
|
28
36
|
|
|
29
|
-
<div class="space-y-6 {className}">
|
|
30
|
-
<PageHeader
|
|
37
|
+
<div class="{density === 'compact' ? 'space-y-4' : 'space-y-6'} {className}">
|
|
38
|
+
<PageHeader
|
|
39
|
+
title={pageTitle}
|
|
40
|
+
{density}
|
|
41
|
+
onBack={() => navigateGuard(() => adminContext.navigate(`/${resourceName}`))}
|
|
42
|
+
>
|
|
31
43
|
{#snippet actions()}
|
|
32
|
-
<ListButton resource={resourceName} hideText />
|
|
44
|
+
<ListButton resource={resourceName} hideText onBeforeNavigate={navigateGuard} />
|
|
33
45
|
{#if headerActions}
|
|
34
46
|
{@render headerActions()}
|
|
35
47
|
{/if}
|
|
36
48
|
{/snippet}
|
|
37
49
|
</PageHeader>
|
|
38
50
|
|
|
39
|
-
<AutoForm
|
|
51
|
+
<AutoForm
|
|
52
|
+
{resourceName}
|
|
53
|
+
mode="create"
|
|
54
|
+
{density}
|
|
55
|
+
{columns}
|
|
56
|
+
showHeader={false}
|
|
57
|
+
onNavigationGuardReady={(guard) => navigateGuard = guard}
|
|
58
|
+
{onSuccess}
|
|
59
|
+
/>
|
|
40
60
|
</div>
|
|
@@ -2,7 +2,10 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
resourceName: string;
|
|
4
4
|
title?: string;
|
|
5
|
+
density?: 'compact' | 'comfortable';
|
|
6
|
+
columns?: 1 | 2 | 3 | 4;
|
|
5
7
|
headerActions?: Snippet;
|
|
8
|
+
onSuccess?: () => void;
|
|
6
9
|
class?: string;
|
|
7
10
|
}
|
|
8
11
|
declare const CreatePage: import("svelte").Component<Props, {}, "">;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
</Sheet.Title>
|
|
33
33
|
</Sheet.Header>
|
|
34
34
|
<div class="flex-1 overflow-y-auto">
|
|
35
|
-
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
35
|
+
<AutoForm {resourceName} {mode} {id} showHeader={false} onSuccess={handleSuccess} />
|
|
36
36
|
</div>
|
|
37
37
|
</Sheet.Content>
|
|
38
38
|
</Sheet.Root>
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
resourceName: string;
|
|
17
17
|
id: string | number;
|
|
18
18
|
title?: string;
|
|
19
|
+
density?: 'compact' | 'comfortable';
|
|
20
|
+
columns?: 1 | 2 | 3 | 4;
|
|
19
21
|
canDelete?: boolean;
|
|
20
22
|
headerActions?: Snippet;
|
|
23
|
+
onSuccess?: () => void;
|
|
21
24
|
class?: string;
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -25,22 +28,30 @@
|
|
|
25
28
|
resourceName,
|
|
26
29
|
id,
|
|
27
30
|
title,
|
|
31
|
+
density = 'comfortable',
|
|
32
|
+
columns = 1,
|
|
28
33
|
canDelete,
|
|
29
34
|
headerActions,
|
|
35
|
+
onSuccess,
|
|
30
36
|
class: className = '',
|
|
31
37
|
}: Props = $props();
|
|
32
38
|
const adminContext = captureAdminContext();
|
|
39
|
+
let navigateGuard = $state<(fn: () => void) => void>((fn) => fn());
|
|
33
40
|
|
|
34
41
|
const resource = $derived(getResource(resourceName));
|
|
35
42
|
const pageTitle = $derived(title ?? `${i18n.t('common.edit')} ${resource.label} #${id}`);
|
|
36
43
|
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
|
-
<div class="space-y-6 {className}">
|
|
40
|
-
<PageHeader
|
|
46
|
+
<div class="{density === 'compact' ? 'space-y-4' : 'space-y-6'} {className}">
|
|
47
|
+
<PageHeader
|
|
48
|
+
title={pageTitle}
|
|
49
|
+
{density}
|
|
50
|
+
onBack={() => navigateGuard(() => adminContext.navigate(`/${resourceName}`))}
|
|
51
|
+
>
|
|
41
52
|
{#snippet actions()}
|
|
42
|
-
<ListButton resource={resourceName} hideText />
|
|
43
|
-
<ShowButton resource={resourceName} recordItemId={id} hideText />
|
|
53
|
+
<ListButton resource={resourceName} hideText onBeforeNavigate={navigateGuard} />
|
|
54
|
+
<ShowButton resource={resourceName} recordItemId={id} hideText onBeforeNavigate={navigateGuard} />
|
|
44
55
|
<RefreshButton resource={resourceName} hideText />
|
|
45
56
|
{#if showDelete !== false}
|
|
46
57
|
<DeleteButton resource={resourceName} recordItemId={id} hideText onSuccess={() => adminContext.navigate(`/${resourceName}`)} />
|
|
@@ -51,5 +62,14 @@
|
|
|
51
62
|
{/snippet}
|
|
52
63
|
</PageHeader>
|
|
53
64
|
|
|
54
|
-
<AutoForm
|
|
65
|
+
<AutoForm
|
|
66
|
+
{resourceName}
|
|
67
|
+
mode="edit"
|
|
68
|
+
{id}
|
|
69
|
+
{density}
|
|
70
|
+
{columns}
|
|
71
|
+
showHeader={false}
|
|
72
|
+
onNavigationGuardReady={(guard) => navigateGuard = guard}
|
|
73
|
+
{onSuccess}
|
|
74
|
+
/>
|
|
55
75
|
</div>
|
|
@@ -3,8 +3,11 @@ interface Props {
|
|
|
3
3
|
resourceName: string;
|
|
4
4
|
id: string | number;
|
|
5
5
|
title?: string;
|
|
6
|
+
density?: 'compact' | 'comfortable';
|
|
7
|
+
columns?: 1 | 2 | 3 | 4;
|
|
6
8
|
canDelete?: boolean;
|
|
7
9
|
headerActions?: Snippet;
|
|
10
|
+
onSuccess?: () => void;
|
|
8
11
|
class?: string;
|
|
9
12
|
}
|
|
10
13
|
declare const EditPage: import("svelte").Component<Props, {}, "">;
|
|
@@ -20,19 +20,23 @@
|
|
|
20
20
|
import { Plus, X } from '@lucide/svelte';
|
|
21
21
|
import type { Snippet } from 'svelte';
|
|
22
22
|
import { getRichTextEditor } from '../editor-config.svelte.js';
|
|
23
|
+
import { cn } from '../utils.js';
|
|
23
24
|
|
|
24
25
|
const i18n = useTranslation();
|
|
25
26
|
|
|
26
|
-
let { field, value, onchange, disabled, invalid = false, errorId, children } = $props<{
|
|
27
|
+
let { field, value, onchange, disabled, invalid = false, errorId, density = 'comfortable', children } = $props<{
|
|
27
28
|
field: FieldDefinition;
|
|
28
29
|
value: unknown;
|
|
29
30
|
onchange: (val: unknown) => void;
|
|
30
31
|
disabled?: boolean;
|
|
31
32
|
invalid?: boolean;
|
|
32
33
|
errorId?: string;
|
|
34
|
+
density?: 'compact' | 'comfortable';
|
|
33
35
|
children?: Snippet;
|
|
34
36
|
}>();
|
|
35
37
|
|
|
38
|
+
const isCompact = $derived(density === 'compact');
|
|
39
|
+
|
|
36
40
|
// Typed accessors
|
|
37
41
|
const strVal = $derived((value as string) ?? '');
|
|
38
42
|
const numVal = $derived(value as number | null | undefined);
|
|
@@ -89,7 +93,15 @@
|
|
|
89
93
|
}
|
|
90
94
|
</script>
|
|
91
95
|
|
|
92
|
-
<div
|
|
96
|
+
<div
|
|
97
|
+
class={cn(
|
|
98
|
+
'space-y-1.5',
|
|
99
|
+
isCompact && 'space-y-1 [&_[data-slot=input]]:h-8 [&_[data-slot=input]]:px-2.5 [&_[data-slot=input]]:py-1 [&_[data-slot=input]]:text-xs [&_[data-slot=select]]:h-8 [&_[data-slot=select]]:px-2.5 [&_[data-slot=select]]:py-1 [&_[data-slot=select]]:text-xs [&_[data-slot=textarea]]:min-h-16 [&_[data-slot=textarea]]:px-2.5 [&_[data-slot=textarea]]:py-1.5 [&_[data-slot=textarea]]:text-xs [&_[data-slot=button]]:h-8 [&_[data-slot=button]]:text-xs'
|
|
100
|
+
)}
|
|
101
|
+
data-svadmin-field
|
|
102
|
+
data-svadmin-field-key={field.key}
|
|
103
|
+
data-density={density}
|
|
104
|
+
>
|
|
93
105
|
<Label for={field.key} id="label-{field.key}" data-svadmin-field-label>
|
|
94
106
|
{field.label}
|
|
95
107
|
{#if field.required}
|
|
@@ -270,6 +282,7 @@
|
|
|
270
282
|
<select
|
|
271
283
|
id={field.key}
|
|
272
284
|
name={field.key}
|
|
285
|
+
data-slot="select"
|
|
273
286
|
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"
|
|
274
287
|
value={strVal}
|
|
275
288
|
onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
|
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
resourceName: string;
|
|
16
16
|
title?: string;
|
|
17
17
|
canCreate?: boolean;
|
|
18
|
+
density?: 'compact' | 'comfortable';
|
|
18
19
|
/** Passthrough: enable row selection checkboxes */
|
|
19
20
|
selectable?: boolean;
|
|
20
21
|
/** Passthrough: custom header actions */
|
|
21
22
|
headerActions?: Snippet;
|
|
23
|
+
/** Optional status tabs slot above filter/table */
|
|
24
|
+
statusTabs?: Snippet;
|
|
25
|
+
/** Optional filter toolbar slot between status tabs and table */
|
|
26
|
+
filterToolbar?: Snippet;
|
|
27
|
+
/** Passthrough: custom batch actions to render when rows are selected */
|
|
28
|
+
batchActions?: Snippet<[{ selectedIds: string[] }]>;
|
|
22
29
|
/** Passthrough: custom cell renderer per field */
|
|
23
30
|
cellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
|
|
24
31
|
/** Passthrough: custom row actions */
|
|
@@ -34,8 +41,12 @@
|
|
|
34
41
|
resourceName,
|
|
35
42
|
title,
|
|
36
43
|
canCreate,
|
|
44
|
+
density = 'comfortable',
|
|
37
45
|
selectable,
|
|
38
46
|
headerActions,
|
|
47
|
+
statusTabs,
|
|
48
|
+
filterToolbar,
|
|
49
|
+
batchActions,
|
|
39
50
|
cellRenderer,
|
|
40
51
|
rowActions,
|
|
41
52
|
emptyState,
|
|
@@ -49,11 +60,11 @@
|
|
|
49
60
|
const showCreate = $derived(canCreate ?? resource.canCreate !== false);
|
|
50
61
|
</script>
|
|
51
62
|
|
|
52
|
-
<div class="space-y-
|
|
53
|
-
<PageHeader title={pageTitle}>
|
|
63
|
+
<div class="{density === 'compact' ? 'space-y-3' : 'space-y-4'} {className}">
|
|
64
|
+
<PageHeader title={pageTitle} {density}>
|
|
54
65
|
{#snippet actions()}
|
|
55
66
|
{#if showCreate}
|
|
56
|
-
<Button onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
|
|
67
|
+
<Button size={density === 'compact' ? 'sm' : 'default'} onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
|
|
57
68
|
<Plus class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.create')}
|
|
58
69
|
</Button>
|
|
59
70
|
{/if}
|
|
@@ -63,9 +74,24 @@
|
|
|
63
74
|
{/snippet}
|
|
64
75
|
</PageHeader>
|
|
65
76
|
|
|
77
|
+
{#if statusTabs}
|
|
78
|
+
<div>
|
|
79
|
+
{@render statusTabs()}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
{#if filterToolbar}
|
|
84
|
+
<div>
|
|
85
|
+
{@render filterToolbar()}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
66
89
|
<AutoTable
|
|
67
90
|
{resourceName}
|
|
91
|
+
showHeader={false}
|
|
92
|
+
{density}
|
|
68
93
|
{selectable}
|
|
94
|
+
{batchActions}
|
|
69
95
|
defaultCellRenderer={cellRenderer}
|
|
70
96
|
{rowActions}
|
|
71
97
|
{emptyState}
|
|
@@ -4,10 +4,19 @@ interface Props {
|
|
|
4
4
|
resourceName: string;
|
|
5
5
|
title?: string;
|
|
6
6
|
canCreate?: boolean;
|
|
7
|
+
density?: 'compact' | 'comfortable';
|
|
7
8
|
/** Passthrough: enable row selection checkboxes */
|
|
8
9
|
selectable?: boolean;
|
|
9
10
|
/** Passthrough: custom header actions */
|
|
10
11
|
headerActions?: Snippet;
|
|
12
|
+
/** Optional status tabs slot above filter/table */
|
|
13
|
+
statusTabs?: Snippet;
|
|
14
|
+
/** Optional filter toolbar slot between status tabs and table */
|
|
15
|
+
filterToolbar?: Snippet;
|
|
16
|
+
/** Passthrough: custom batch actions to render when rows are selected */
|
|
17
|
+
batchActions?: Snippet<[{
|
|
18
|
+
selectedIds: string[];
|
|
19
|
+
}]>;
|
|
11
20
|
/** Passthrough: custom cell renderer per field */
|
|
12
21
|
cellRenderer?: Snippet<[{
|
|
13
22
|
field: FieldDefinition;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{mode === 'create' ? `${i18n.t('common.create')}${resource.label}` : `${i18n.t('common.edit')}${resource.label}`}
|
|
33
33
|
</Dialog.DialogTitle>
|
|
34
34
|
</Dialog.DialogHeader>
|
|
35
|
-
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
35
|
+
<AutoForm {resourceName} {mode} {id} showHeader={false} onSuccess={handleSuccess} />
|
|
36
36
|
</Dialog.DialogContent>
|
|
37
37
|
</Dialog.Dialog>
|
|
38
38
|
{/if}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { ArrowLeft } from '@lucide/svelte';
|
|
3
4
|
import Breadcrumbs from './Breadcrumbs.svelte';
|
|
5
|
+
import { Button } from './ui/button/index.js';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
4
7
|
|
|
5
8
|
interface Props {
|
|
6
9
|
title: string;
|
|
7
10
|
description?: string;
|
|
8
11
|
actions?: Snippet;
|
|
12
|
+
extra?: Snippet;
|
|
13
|
+
tags?: Snippet;
|
|
14
|
+
back?: Snippet;
|
|
15
|
+
onBack?: () => void;
|
|
16
|
+
backLabel?: string;
|
|
9
17
|
showBreadcrumbs?: boolean;
|
|
18
|
+
density?: 'compact' | 'comfortable';
|
|
10
19
|
class?: string;
|
|
11
20
|
}
|
|
12
21
|
|
|
@@ -14,25 +23,63 @@
|
|
|
14
23
|
title,
|
|
15
24
|
description,
|
|
16
25
|
actions,
|
|
26
|
+
extra,
|
|
27
|
+
tags,
|
|
28
|
+
back,
|
|
29
|
+
onBack,
|
|
30
|
+
backLabel = 'Back',
|
|
17
31
|
showBreadcrumbs = true,
|
|
32
|
+
density = 'comfortable',
|
|
18
33
|
class: className = '',
|
|
19
34
|
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const isCompact = $derived(density === 'compact');
|
|
20
37
|
</script>
|
|
21
38
|
|
|
22
|
-
<div class=
|
|
39
|
+
<div class={cn(isCompact ? 'space-y-1.5' : 'space-y-4', className)}>
|
|
23
40
|
{#if showBreadcrumbs}
|
|
24
|
-
<Breadcrumbs />
|
|
41
|
+
<Breadcrumbs class={isCompact ? 'mb-1 text-xs' : 'mb-4'} />
|
|
25
42
|
{/if}
|
|
26
|
-
<div class=
|
|
27
|
-
<div>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
<div class={cn('flex flex-col sm:flex-row sm:items-center sm:justify-between', isCompact ? 'gap-1.5' : 'gap-2')}>
|
|
44
|
+
<div class="flex items-center gap-2.5 min-w-0">
|
|
45
|
+
{#if back}
|
|
46
|
+
{@render back()}
|
|
47
|
+
{:else if onBack}
|
|
48
|
+
<Button
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="icon-xs"
|
|
51
|
+
class="shrink-0 text-muted-foreground hover:text-foreground"
|
|
52
|
+
onclick={onBack}
|
|
53
|
+
aria-label={backLabel}
|
|
54
|
+
title={backLabel}
|
|
55
|
+
>
|
|
56
|
+
<ArrowLeft class="size-4" />
|
|
57
|
+
</Button>
|
|
31
58
|
{/if}
|
|
59
|
+
<div class="min-w-0">
|
|
60
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
61
|
+
<h1 class={cn('font-semibold text-foreground truncate', isCompact ? 'text-lg' : 'text-xl')}>
|
|
62
|
+
{title}
|
|
63
|
+
</h1>
|
|
64
|
+
{#if tags}
|
|
65
|
+
<div class="inline-flex items-center gap-1.5">
|
|
66
|
+
{@render tags()}
|
|
67
|
+
</div>
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
{#if description}
|
|
71
|
+
<p class={cn('text-muted-foreground', isCompact ? 'text-xs' : 'text-sm')}>{description}</p>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
32
74
|
</div>
|
|
33
|
-
{#if actions}
|
|
34
|
-
<div class=
|
|
35
|
-
{
|
|
75
|
+
{#if actions || extra}
|
|
76
|
+
<div class={cn('flex items-center shrink-0', isCompact ? 'gap-1.5' : 'gap-2')}>
|
|
77
|
+
{#if extra}
|
|
78
|
+
{@render extra()}
|
|
79
|
+
{/if}
|
|
80
|
+
{#if actions}
|
|
81
|
+
{@render actions()}
|
|
82
|
+
{/if}
|
|
36
83
|
</div>
|
|
37
84
|
{/if}
|
|
38
85
|
</div>
|
|
@@ -3,7 +3,13 @@ interface Props {
|
|
|
3
3
|
title: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
actions?: Snippet;
|
|
6
|
+
extra?: Snippet;
|
|
7
|
+
tags?: Snippet;
|
|
8
|
+
back?: Snippet;
|
|
9
|
+
onBack?: () => void;
|
|
10
|
+
backLabel?: string;
|
|
6
11
|
showBreadcrumbs?: boolean;
|
|
12
|
+
density?: 'compact' | 'comfortable';
|
|
7
13
|
class?: string;
|
|
8
14
|
}
|
|
9
15
|
declare const PageHeader: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { captureAdminContext, useShow, getResource } from '@svadmin/core';
|
|
3
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
4
5
|
import * as Card from './ui/card/index.js';
|
|
5
6
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
6
7
|
import PageHeader from './PageHeader.svelte';
|
|
7
|
-
import { getDisplayComponent } from './fieldComponentMap';
|
|
8
|
+
import { getDisplayComponent } from './fieldComponentMap.js';
|
|
8
9
|
import ListButton from './buttons/ListButton.svelte';
|
|
9
10
|
import EditButton from './buttons/EditButton.svelte';
|
|
10
11
|
import DeleteButton from './buttons/DeleteButton.svelte';
|
|
@@ -13,17 +14,54 @@
|
|
|
13
14
|
|
|
14
15
|
const i18n = useTranslation();
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
interface Props {
|
|
18
|
+
resourceName: string;
|
|
19
|
+
id: string | number;
|
|
20
|
+
density?: 'compact' | 'comfortable';
|
|
21
|
+
layout?: 'list' | 'grid';
|
|
22
|
+
columns?: 1 | 2 | 3 | 4;
|
|
23
|
+
bordered?: boolean;
|
|
24
|
+
headerActions?: Snippet;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
resourceName,
|
|
31
|
+
id,
|
|
32
|
+
density = 'comfortable',
|
|
33
|
+
layout = 'list',
|
|
34
|
+
columns = 2,
|
|
35
|
+
bordered = false,
|
|
36
|
+
headerActions,
|
|
37
|
+
children,
|
|
38
|
+
class: className = '',
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
17
41
|
const adminContext = captureAdminContext();
|
|
42
|
+
const isCompact = $derived(density === 'compact');
|
|
18
43
|
|
|
19
44
|
const resource = $derived(getResource(resourceName));
|
|
20
45
|
const showFields = $derived(resource.fields.filter(f => f.showInShow !== false));
|
|
21
46
|
|
|
22
47
|
const query = useShow({ get resource() { return resourceName; }, get id() { return id; } });
|
|
48
|
+
|
|
49
|
+
const gridColumnClass = $derived.by(() => {
|
|
50
|
+
switch (columns) {
|
|
51
|
+
case 4: return 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4';
|
|
52
|
+
case 3: return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
|
|
53
|
+
case 2: return 'grid-cols-1 sm:grid-cols-2';
|
|
54
|
+
default: return 'grid-cols-1';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
23
57
|
</script>
|
|
24
58
|
|
|
25
|
-
<div class="space-y-
|
|
26
|
-
<PageHeader
|
|
59
|
+
<div class="{isCompact ? 'space-y-3' : 'space-y-4'} {className}">
|
|
60
|
+
<PageHeader
|
|
61
|
+
title="{resource.label} {i18n.t('common.detail')} #{id}"
|
|
62
|
+
{density}
|
|
63
|
+
onBack={() => adminContext.navigate(`/${resourceName}`)}
|
|
64
|
+
>
|
|
27
65
|
{#snippet actions()}
|
|
28
66
|
<ListButton resource={resourceName} hideText />
|
|
29
67
|
{#if resource.canEdit !== false}
|
|
@@ -36,6 +74,9 @@
|
|
|
36
74
|
<DeleteButton resource={resourceName} recordItemId={id} hideText onSuccess={() => adminContext.navigate(`/${resourceName}`)} />
|
|
37
75
|
{/if}
|
|
38
76
|
<RefreshButton resource={resourceName} hideText />
|
|
77
|
+
{#if headerActions}
|
|
78
|
+
{@render headerActions()}
|
|
79
|
+
{/if}
|
|
39
80
|
{/snippet}
|
|
40
81
|
</PageHeader>
|
|
41
82
|
|
|
@@ -43,7 +84,7 @@
|
|
|
43
84
|
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
44
85
|
<Card.Content class="p-0">
|
|
45
86
|
{#each showFields.slice(0, 6) as _, i (i)}
|
|
46
|
-
<div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
87
|
+
<div class="flex flex-col sm:flex-row {isCompact ? 'px-4 py-2' : 'px-4 sm:px-6 py-3 sm:py-4'} {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
47
88
|
<Skeleton class="h-4 w-1/2 sm:w-1/4" />
|
|
48
89
|
<Skeleton class="h-4 w-3/4 sm:w-2/5 mt-1 sm:mt-0 sm:ml-auto" />
|
|
49
90
|
</div>
|
|
@@ -51,28 +92,58 @@
|
|
|
51
92
|
</Card.Content>
|
|
52
93
|
</Card.Root>
|
|
53
94
|
{:else if query.data?.data}
|
|
54
|
-
|
|
55
|
-
<Card.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
{value}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
{#if layout === 'grid'}
|
|
96
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
97
|
+
<Card.Content class="p-0">
|
|
98
|
+
<dl class="grid divide-y divide-border/20 sm:divide-y-0 {gridColumnClass}">
|
|
99
|
+
{#each showFields as field, i (i)}
|
|
100
|
+
{@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
|
|
101
|
+
{@const DisplayComponent = getDisplayComponent(field.type)}
|
|
102
|
+
<div class="flex flex-col {bordered ? 'border-b border-r border-border/30 last:border-b-0 p-3 sm:p-4' : 'p-3 sm:p-4 border-b border-border/10'}">
|
|
103
|
+
<dt class="text-xs font-medium text-muted-foreground mb-1">{field.label}</dt>
|
|
104
|
+
<dd class="text-xs sm:text-sm text-foreground font-medium break-words">
|
|
105
|
+
{#if DisplayComponent && value != null}
|
|
106
|
+
<DisplayComponent
|
|
107
|
+
{value}
|
|
108
|
+
options={field.options}
|
|
109
|
+
resourceName={field.resource}
|
|
110
|
+
/>
|
|
111
|
+
{:else}
|
|
112
|
+
{value != null ? String(value) : '—'}
|
|
113
|
+
{/if}
|
|
114
|
+
</dd>
|
|
115
|
+
</div>
|
|
116
|
+
{/each}
|
|
117
|
+
</dl>
|
|
118
|
+
</Card.Content>
|
|
119
|
+
</Card.Root>
|
|
120
|
+
{:else}
|
|
121
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
122
|
+
<Card.Content class="p-0 divide-y divide-border/20">
|
|
123
|
+
{#each showFields as field, i (i)}
|
|
124
|
+
{@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
|
|
125
|
+
{@const DisplayComponent = getDisplayComponent(field.type)}
|
|
126
|
+
<div class="flex flex-col sm:flex-row {isCompact ? 'px-4 py-2 sm:px-5 sm:py-2' : 'px-4 sm:px-6 py-3 sm:py-3.5'} {i % 2 === 1 ? 'bg-muted/10' : ''}">
|
|
127
|
+
<div class="sm:w-1/3 {isCompact ? 'text-xs' : 'text-xs sm:text-sm'} font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
|
|
128
|
+
<div class="sm:w-2/3 {isCompact ? 'text-xs sm:text-sm' : 'text-sm'} text-foreground">
|
|
129
|
+
{#if DisplayComponent && value != null}
|
|
130
|
+
<DisplayComponent
|
|
131
|
+
{value}
|
|
132
|
+
options={field.options}
|
|
133
|
+
resourceName={field.resource}
|
|
134
|
+
/>
|
|
135
|
+
{:else}
|
|
136
|
+
{value != null ? String(value) : '—'}
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
71
139
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</Card.
|
|
75
|
-
|
|
140
|
+
{/each}
|
|
141
|
+
</Card.Content>
|
|
142
|
+
</Card.Root>
|
|
143
|
+
{/if}
|
|
144
|
+
{#if children}
|
|
145
|
+
{@render children()}
|
|
146
|
+
{/if}
|
|
76
147
|
{:else}
|
|
77
148
|
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
78
149
|
<Card.Content class="p-8 text-center">
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
2
3
|
resourceName: string;
|
|
3
4
|
id: string | number;
|
|
5
|
+
density?: 'compact' | 'comfortable';
|
|
6
|
+
layout?: 'list' | 'grid';
|
|
7
|
+
columns?: 1 | 2 | 3 | 4;
|
|
8
|
+
bordered?: boolean;
|
|
9
|
+
headerActions?: Snippet;
|
|
10
|
+
children?: Snippet;
|
|
4
11
|
class?: string;
|
|
5
|
-
}
|
|
6
|
-
declare const ShowPage: import("svelte").Component
|
|
12
|
+
}
|
|
13
|
+
declare const ShowPage: import("svelte").Component<Props, {}, "">;
|
|
7
14
|
type ShowPage = ReturnType<typeof ShowPage>;
|
|
8
15
|
export default ShowPage;
|