@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.
Files changed (51) hide show
  1. package/dist/components/AutoForm.svelte +105 -54
  2. package/dist/components/AutoForm.svelte.d.ts +4 -0
  3. package/dist/components/AutoTable.svelte +223 -106
  4. package/dist/components/AutoTable.svelte.d.ts +5 -0
  5. package/dist/components/Breadcrumbs.svelte +3 -2
  6. package/dist/components/Breadcrumbs.svelte.d.ts +1 -0
  7. package/dist/components/CreatePage.svelte +25 -5
  8. package/dist/components/CreatePage.svelte.d.ts +3 -0
  9. package/dist/components/DrawerForm.svelte +1 -1
  10. package/dist/components/EditPage.svelte +25 -5
  11. package/dist/components/EditPage.svelte.d.ts +3 -0
  12. package/dist/components/FieldRenderer.svelte +15 -2
  13. package/dist/components/FieldRenderer.svelte.d.ts +1 -0
  14. package/dist/components/ListPage.svelte +29 -3
  15. package/dist/components/ListPage.svelte.d.ts +9 -0
  16. package/dist/components/ModalForm.svelte +1 -1
  17. package/dist/components/PageHeader.svelte +57 -10
  18. package/dist/components/PageHeader.svelte.d.ts +6 -0
  19. package/dist/components/ShowPage.svelte +97 -26
  20. package/dist/components/ShowPage.svelte.d.ts +10 -3
  21. package/dist/components/buttons/CloneButton.svelte +23 -2
  22. package/dist/components/buttons/CloneButton.svelte.d.ts +3 -0
  23. package/dist/components/buttons/CreateButton.svelte +22 -2
  24. package/dist/components/buttons/CreateButton.svelte.d.ts +3 -0
  25. package/dist/components/buttons/DeleteButton.svelte +23 -3
  26. package/dist/components/buttons/DeleteButton.svelte.d.ts +3 -0
  27. package/dist/components/buttons/EditButton.svelte +23 -2
  28. package/dist/components/buttons/EditButton.svelte.d.ts +3 -0
  29. package/dist/components/buttons/ExportButton.svelte +22 -3
  30. package/dist/components/buttons/ExportButton.svelte.d.ts +3 -0
  31. package/dist/components/buttons/ImportButton.svelte +39 -17
  32. package/dist/components/buttons/ImportButton.svelte.d.ts +3 -0
  33. package/dist/components/buttons/ListButton.svelte +29 -4
  34. package/dist/components/buttons/ListButton.svelte.d.ts +4 -0
  35. package/dist/components/buttons/RefreshButton.svelte +22 -2
  36. package/dist/components/buttons/RefreshButton.svelte.d.ts +3 -0
  37. package/dist/components/buttons/SaveButton.svelte +23 -2
  38. package/dist/components/buttons/SaveButton.svelte.d.ts +3 -0
  39. package/dist/components/buttons/ShowButton.svelte +32 -4
  40. package/dist/components/buttons/ShowButton.svelte.d.ts +4 -0
  41. package/dist/components/content/DescriptionList.svelte +125 -15
  42. package/dist/components/content/DescriptionList.svelte.d.ts +6 -2
  43. package/dist/components/content/FilterToolbar.svelte +81 -8
  44. package/dist/components/content/FilterToolbar.svelte.d.ts +7 -1
  45. package/dist/components/content/StatusTabs.svelte +156 -0
  46. package/dist/components/content/StatusTabs.svelte.d.ts +18 -0
  47. package/dist/components/content/index.d.ts +2 -0
  48. package/dist/components/content/index.js +1 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +1 -0
  51. 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="mb-4">
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,6 +1,7 @@
1
1
  import { type MenuItem } from '@svadmin/core';
2
2
  type $$ComponentProps = {
3
3
  menu?: MenuItem[];
4
+ class?: string;
4
5
  };
5
6
  declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "">;
6
7
  type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
@@ -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 title={pageTitle}>
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 {resourceName} mode="create" />
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 title={pageTitle}>
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 {resourceName} mode="edit" {id} />
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 class="space-y-1.5" data-svadmin-field data-svadmin-field-key={field.key}>
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)}
@@ -8,6 +8,7 @@ type $$ComponentProps = {
8
8
  disabled?: boolean;
9
9
  invalid?: boolean;
10
10
  errorId?: string;
11
+ density?: 'compact' | 'comfortable';
11
12
  children?: Snippet;
12
13
  };
13
14
  declare const FieldRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -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-6 {className}">
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="space-y-4 {className}">
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="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
27
- <div>
28
- <h1 class="text-xl font-semibold tracking-tight text-foreground">{title}</h1>
29
- {#if description}
30
- <p class="text-sm text-muted-foreground">{description}</p>
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="flex items-center gap-2">
35
- {@render actions()}
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
- let { resourceName, id, class: className = '' } = $props<{ resourceName: string; id: string | number; class?: string }>();
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-6 {className}">
26
- <PageHeader title="{resource.label} {i18n.t('common.detail')}">
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
- <Card.Root class="overflow-hidden border-border/40 shadow-sm">
55
- <Card.Content class="p-0">
56
- {#each showFields as field, i (i)}
57
- {@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
58
- {@const DisplayComponent = getDisplayComponent(field.type)}
59
- <div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
60
- <div class="sm:w-1/3 text-xs sm:text-sm font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
61
- <div class="sm:w-2/3 text-sm text-foreground">
62
- {#if DisplayComponent && value != null}
63
- <DisplayComponent
64
- {value}
65
- options={field.options}
66
- resourceName={field.resource}
67
- />
68
- {:else}
69
- {value != null ? String(value) : '—'}
70
- {/if}
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
- </div>
73
- {/each}
74
- </Card.Content>
75
- </Card.Root>
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 $$ComponentProps = {
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<$$ComponentProps, {}, "">;
12
+ }
13
+ declare const ShowPage: import("svelte").Component<Props, {}, "">;
7
14
  type ShowPage = ReturnType<typeof ShowPage>;
8
15
  export default ShowPage;