@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import { useNavigation, useCan, useTranslation } from '@svadmin/core';
|
|
3
4
|
import { Button } from '../ui/button/index.js';
|
|
4
5
|
import { Eye } from '@lucide/svelte';
|
|
@@ -7,11 +8,23 @@
|
|
|
7
8
|
|
|
8
9
|
const i18n = useTranslation();
|
|
9
10
|
|
|
10
|
-
let {
|
|
11
|
+
let {
|
|
12
|
+
resource,
|
|
13
|
+
recordItemId,
|
|
14
|
+
label,
|
|
15
|
+
children,
|
|
16
|
+
hideText = false,
|
|
17
|
+
accessControl = { enabled: true, hideIfUnauthorized: true },
|
|
18
|
+
onBeforeNavigate,
|
|
19
|
+
class: className = '',
|
|
20
|
+
} = $props<{
|
|
11
21
|
resource: string;
|
|
12
22
|
recordItemId: string | number;
|
|
23
|
+
label?: string;
|
|
24
|
+
children?: Snippet;
|
|
13
25
|
hideText?: boolean;
|
|
14
26
|
accessControl?: ButtonAccessControl;
|
|
27
|
+
onBeforeNavigate?: (navigate: () => void) => void;
|
|
15
28
|
class?: string;
|
|
16
29
|
}>();
|
|
17
30
|
|
|
@@ -24,18 +37,33 @@
|
|
|
24
37
|
queryOptions: { enabled: accessControl?.enabled ?? true }
|
|
25
38
|
}));
|
|
26
39
|
const hidden = $derived(accessControl?.hideIfUnauthorized && !can.allowed);
|
|
40
|
+
const displayText = $derived(label ?? i18n.t('common.detail'));
|
|
41
|
+
|
|
42
|
+
function navigateToRecord() {
|
|
43
|
+
const navigate = () => nav.show(resource, recordItemId);
|
|
44
|
+
if (onBeforeNavigate) onBeforeNavigate(navigate);
|
|
45
|
+
else navigate();
|
|
46
|
+
}
|
|
27
47
|
</script>
|
|
28
48
|
|
|
29
49
|
{#if !hidden}
|
|
30
|
-
|
|
31
50
|
<Button
|
|
32
51
|
variant="ghost"
|
|
33
52
|
size={hideText ? 'icon' : 'sm'}
|
|
34
53
|
class={className}
|
|
54
|
+
aria-label={hideText ? displayText : undefined}
|
|
35
55
|
disabled={!can.allowed}
|
|
36
|
-
onclick={
|
|
56
|
+
onclick={navigateToRecord}
|
|
37
57
|
>
|
|
38
58
|
<Eye class="h-4 w-4" />
|
|
39
|
-
{#if !hideText}
|
|
59
|
+
{#if !hideText}
|
|
60
|
+
<span class="ml-1">
|
|
61
|
+
{#if children}
|
|
62
|
+
{@render children()}
|
|
63
|
+
{:else}
|
|
64
|
+
{displayText}
|
|
65
|
+
{/if}
|
|
66
|
+
</span>
|
|
67
|
+
{/if}
|
|
40
68
|
</Button>
|
|
41
69
|
{/if}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { ButtonAccessControl } from './access-control';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
resource: string;
|
|
4
5
|
recordItemId: string | number;
|
|
6
|
+
label?: string;
|
|
7
|
+
children?: Snippet;
|
|
5
8
|
hideText?: boolean;
|
|
6
9
|
accessControl?: ButtonAccessControl;
|
|
10
|
+
onBeforeNavigate?: (navigate: () => void) => void;
|
|
7
11
|
class?: string;
|
|
8
12
|
};
|
|
9
13
|
declare const ShowButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -1,27 +1,137 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
2
4
|
export interface DescriptionItem {
|
|
3
5
|
label: string;
|
|
4
|
-
value:
|
|
6
|
+
value: unknown;
|
|
5
7
|
description?: string;
|
|
6
8
|
href?: string;
|
|
9
|
+
span?: number;
|
|
7
10
|
}
|
|
11
|
+
|
|
8
12
|
interface Props {
|
|
9
13
|
items?: DescriptionItem[];
|
|
10
|
-
columns?: 1 | 2;
|
|
14
|
+
columns?: 1 | 2 | 3 | 4;
|
|
15
|
+
layout?: 'vertical' | 'horizontal';
|
|
16
|
+
bordered?: boolean;
|
|
17
|
+
density?: 'compact' | 'comfortable';
|
|
11
18
|
class?: string;
|
|
12
19
|
}
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
items = [],
|
|
23
|
+
columns = 1,
|
|
24
|
+
layout = 'vertical',
|
|
25
|
+
bordered = false,
|
|
26
|
+
density = 'comfortable',
|
|
27
|
+
class: className = '',
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const isCompact = $derived(density === 'compact');
|
|
31
|
+
const isHorizontal = $derived(layout === 'horizontal');
|
|
32
|
+
|
|
33
|
+
function spanClass(span: number | undefined): string {
|
|
34
|
+
if (!span || span <= 1) return '';
|
|
35
|
+
if (span >= 4) return 'sm:col-span-4';
|
|
36
|
+
if (span === 3) return 'sm:col-span-3';
|
|
37
|
+
return 'sm:col-span-2';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const columnClass = $derived.by(() => {
|
|
41
|
+
switch (columns) {
|
|
42
|
+
case 4:
|
|
43
|
+
return 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4';
|
|
44
|
+
case 3:
|
|
45
|
+
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
|
|
46
|
+
case 2:
|
|
47
|
+
return 'grid-cols-1 sm:grid-cols-2';
|
|
48
|
+
default:
|
|
49
|
+
return 'grid-cols-1';
|
|
50
|
+
}
|
|
51
|
+
});
|
|
15
52
|
</script>
|
|
16
53
|
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
{#if bordered}
|
|
55
|
+
<div class={cn('overflow-hidden rounded-lg border border-border bg-card text-foreground', className)}>
|
|
56
|
+
<dl class={cn('grid divide-y divide-border/60 sm:divide-y-0', columnClass)}>
|
|
57
|
+
{#each items as item (item.label)}
|
|
58
|
+
<div
|
|
59
|
+
class={cn(
|
|
60
|
+
'flex flex-col border-b border-r border-border/40 last:border-b-0',
|
|
61
|
+
isHorizontal ? 'sm:flex-row' : '',
|
|
62
|
+
spanClass(Math.min(item.span ?? 1, columns)),
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<dt
|
|
66
|
+
class={cn(
|
|
67
|
+
'bg-muted/30 font-medium text-muted-foreground shrink-0 border-b sm:border-b-0 sm:border-r border-border/30',
|
|
68
|
+
isCompact ? 'px-3 py-1.5 text-xs' : 'px-4 py-2.5 text-xs sm:text-sm',
|
|
69
|
+
isHorizontal ? 'sm:w-1/3' : '',
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{item.label}
|
|
73
|
+
</dt>
|
|
74
|
+
<dd
|
|
75
|
+
class={cn(
|
|
76
|
+
'break-words font-medium text-foreground bg-card flex-1 min-w-0',
|
|
77
|
+
isCompact ? 'px-3 py-1.5 text-xs' : 'px-4 py-2.5 text-xs sm:text-sm',
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{#if item.href}
|
|
81
|
+
<a class="text-primary underline-offset-4 hover:underline" href={item.href}>{String(item.value ?? '—')}</a>
|
|
82
|
+
{:else}
|
|
83
|
+
{item.value != null ? String(item.value) : '—'}
|
|
84
|
+
{/if}
|
|
85
|
+
{#if item.description}
|
|
86
|
+
<p class="mt-0.5 text-xs font-normal text-muted-foreground">{item.description}</p>
|
|
87
|
+
{/if}
|
|
88
|
+
</dd>
|
|
89
|
+
</div>
|
|
90
|
+
{/each}
|
|
91
|
+
</dl>
|
|
92
|
+
</div>
|
|
93
|
+
{:else}
|
|
94
|
+
<dl
|
|
95
|
+
class={cn(
|
|
96
|
+
'grid',
|
|
97
|
+
columnClass,
|
|
98
|
+
isCompact ? 'gap-x-4 gap-y-3' : 'gap-x-6 gap-y-5',
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{#each items as item (item.label)}
|
|
103
|
+
<div
|
|
104
|
+
class={cn(
|
|
105
|
+
'min-w-0',
|
|
106
|
+
isHorizontal ? 'flex items-baseline gap-2' : '',
|
|
107
|
+
spanClass(Math.min(item.span ?? 1, columns)),
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<dt
|
|
111
|
+
class={cn(
|
|
112
|
+
'font-medium text-muted-foreground',
|
|
113
|
+
isCompact ? 'text-xs' : 'text-xs',
|
|
114
|
+
isHorizontal ? 'shrink-0 min-w-20' : '',
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{item.label}
|
|
118
|
+
</dt>
|
|
119
|
+
<dd
|
|
120
|
+
class={cn(
|
|
121
|
+
'break-words font-medium text-foreground min-w-0 flex-1',
|
|
122
|
+
isHorizontal ? 'text-xs sm:text-sm' : isCompact ? 'mt-0.5 text-xs sm:text-sm' : 'mt-1 text-sm',
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{#if item.href}
|
|
126
|
+
<a class="text-primary underline-offset-4 hover:underline" href={item.href}>{String(item.value ?? '—')}</a>
|
|
127
|
+
{:else}
|
|
128
|
+
{item.value != null ? String(item.value) : '—'}
|
|
129
|
+
{/if}
|
|
130
|
+
{#if item.description}
|
|
131
|
+
<p class="mt-1 text-xs font-normal text-muted-foreground">{item.description}</p>
|
|
132
|
+
{/if}
|
|
133
|
+
</dd>
|
|
134
|
+
</div>
|
|
135
|
+
{/each}
|
|
136
|
+
</dl>
|
|
137
|
+
{/if}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export interface DescriptionItem {
|
|
2
2
|
label: string;
|
|
3
|
-
value:
|
|
3
|
+
value: unknown;
|
|
4
4
|
description?: string;
|
|
5
5
|
href?: string;
|
|
6
|
+
span?: number;
|
|
6
7
|
}
|
|
7
8
|
interface Props {
|
|
8
9
|
items?: DescriptionItem[];
|
|
9
|
-
columns?: 1 | 2;
|
|
10
|
+
columns?: 1 | 2 | 3 | 4;
|
|
11
|
+
layout?: 'vertical' | 'horizontal';
|
|
12
|
+
bordered?: boolean;
|
|
13
|
+
density?: 'compact' | 'comfortable';
|
|
10
14
|
class?: string;
|
|
11
15
|
}
|
|
12
16
|
declare const DescriptionList: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,32 +1,105 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import { Search, X } from '@lucide/svelte';
|
|
3
|
+
import { Search, X, SlidersHorizontal, ChevronDown, ChevronUp } from '@lucide/svelte';
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
5
|
import { Input } from '../ui/input/index.js';
|
|
6
|
+
import { Badge } from '../ui/badge/index.js';
|
|
7
|
+
import { cn } from '../../utils.js';
|
|
8
|
+
|
|
6
9
|
interface Props {
|
|
7
10
|
query?: string;
|
|
8
11
|
placeholder?: string;
|
|
9
12
|
clearLabel?: string;
|
|
13
|
+
advancedLabel?: string;
|
|
14
|
+
showSearch?: boolean;
|
|
15
|
+
density?: 'compact' | 'comfortable';
|
|
16
|
+
activeFilterCount?: number;
|
|
17
|
+
advancedOpen?: boolean;
|
|
10
18
|
filters?: Snippet;
|
|
11
19
|
actions?: Snippet;
|
|
20
|
+
advanced?: Snippet;
|
|
12
21
|
class?: string;
|
|
13
22
|
}
|
|
23
|
+
|
|
14
24
|
let {
|
|
15
25
|
query = $bindable(''),
|
|
16
26
|
placeholder = 'Search',
|
|
17
27
|
clearLabel = 'Clear search',
|
|
28
|
+
advancedLabel = 'Filters',
|
|
29
|
+
showSearch = true,
|
|
30
|
+
density = 'compact',
|
|
31
|
+
activeFilterCount = 0,
|
|
32
|
+
advancedOpen = $bindable(false),
|
|
18
33
|
filters,
|
|
19
34
|
actions,
|
|
35
|
+
advanced,
|
|
20
36
|
class: className = '',
|
|
21
37
|
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const isCompact = $derived(density === 'compact');
|
|
22
40
|
</script>
|
|
23
41
|
|
|
24
|
-
<div class={'
|
|
25
|
-
<div class="
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
<div class={cn('space-y-2', className)}>
|
|
43
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
44
|
+
{#if showSearch}
|
|
45
|
+
<div class="relative min-w-0 flex-1">
|
|
46
|
+
<Search class={cn('pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground', isCompact ? 'size-3.5' : 'size-4')} />
|
|
47
|
+
<Input
|
|
48
|
+
bind:value={query}
|
|
49
|
+
{placeholder}
|
|
50
|
+
class={cn('pl-9 pr-9', isCompact ? 'h-8 text-xs' : 'h-9 text-sm')}
|
|
51
|
+
aria-label={placeholder}
|
|
52
|
+
/>
|
|
53
|
+
{#if query}
|
|
54
|
+
<Button
|
|
55
|
+
variant="ghost"
|
|
56
|
+
size="icon-xs"
|
|
57
|
+
class="absolute right-1 top-1/2 -translate-y-1/2"
|
|
58
|
+
aria-label={clearLabel}
|
|
59
|
+
title={clearLabel}
|
|
60
|
+
onclick={() => query = ''}
|
|
61
|
+
>
|
|
62
|
+
<X class="size-3.5" />
|
|
63
|
+
</Button>
|
|
64
|
+
{/if}
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
{#if filters}
|
|
68
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
69
|
+
{@render filters()}
|
|
70
|
+
</div>
|
|
71
|
+
{/if}
|
|
72
|
+
{#if advanced}
|
|
73
|
+
<Button
|
|
74
|
+
variant={advancedOpen ? 'secondary' : 'outline'}
|
|
75
|
+
size={isCompact ? 'sm' : 'default'}
|
|
76
|
+
class={cn('gap-1.5 shrink-0', isCompact ? 'h-8 text-xs px-2.5' : '')}
|
|
77
|
+
onclick={() => advancedOpen = !advancedOpen}
|
|
78
|
+
aria-expanded={advancedOpen}
|
|
79
|
+
>
|
|
80
|
+
<SlidersHorizontal class="size-3.5" />
|
|
81
|
+
<span>{advancedLabel}</span>
|
|
82
|
+
{#if activeFilterCount > 0}
|
|
83
|
+
<Badge variant="secondary" class="ml-0.5 h-4 min-w-4 px-1 text-[10px] leading-none font-semibold">
|
|
84
|
+
{activeFilterCount}
|
|
85
|
+
</Badge>
|
|
86
|
+
{/if}
|
|
87
|
+
{#if advancedOpen}
|
|
88
|
+
<ChevronUp class="size-3 text-muted-foreground ml-0.5" />
|
|
89
|
+
{:else}
|
|
90
|
+
<ChevronDown class="size-3 text-muted-foreground ml-0.5" />
|
|
91
|
+
{/if}
|
|
92
|
+
</Button>
|
|
93
|
+
{/if}
|
|
94
|
+
{#if actions}
|
|
95
|
+
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
|
96
|
+
{@render actions()}
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
29
99
|
</div>
|
|
30
|
-
{#if
|
|
31
|
-
|
|
100
|
+
{#if advanced && advancedOpen}
|
|
101
|
+
<div class="rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
102
|
+
{@render advanced()}
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
32
105
|
</div>
|
|
@@ -3,10 +3,16 @@ interface Props {
|
|
|
3
3
|
query?: string;
|
|
4
4
|
placeholder?: string;
|
|
5
5
|
clearLabel?: string;
|
|
6
|
+
advancedLabel?: string;
|
|
7
|
+
showSearch?: boolean;
|
|
8
|
+
density?: 'compact' | 'comfortable';
|
|
9
|
+
activeFilterCount?: number;
|
|
10
|
+
advancedOpen?: boolean;
|
|
6
11
|
filters?: Snippet;
|
|
7
12
|
actions?: Snippet;
|
|
13
|
+
advanced?: Snippet;
|
|
8
14
|
class?: string;
|
|
9
15
|
}
|
|
10
|
-
declare const FilterToolbar: import("svelte").Component<Props, {}, "query">;
|
|
16
|
+
declare const FilterToolbar: import("svelte").Component<Props, {}, "query" | "advancedOpen">;
|
|
11
17
|
type FilterToolbar = ReturnType<typeof FilterToolbar>;
|
|
12
18
|
export default FilterToolbar;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
export interface StatusTabItem {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
count?: number | string;
|
|
8
|
+
tone?: 'default' | 'success' | 'warning' | 'destructive' | 'info' | 'muted';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
items: StatusTabItem[];
|
|
14
|
+
value?: string;
|
|
15
|
+
density?: 'compact' | 'comfortable';
|
|
16
|
+
variant?: 'pills' | 'underline' | 'segmented';
|
|
17
|
+
onchange?: (key: string) => void;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
items = [],
|
|
23
|
+
value = $bindable(items[0]?.key ?? ''),
|
|
24
|
+
density = 'compact',
|
|
25
|
+
variant = 'pills',
|
|
26
|
+
onchange,
|
|
27
|
+
class: className = '',
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
function selectTab(key: string, disabled?: boolean) {
|
|
31
|
+
if (disabled || key === value) return;
|
|
32
|
+
value = key;
|
|
33
|
+
onchange?.(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type TabKeyboardEvent = KeyboardEvent & { currentTarget: EventTarget & HTMLButtonElement };
|
|
37
|
+
|
|
38
|
+
function handleKeydown(event: TabKeyboardEvent) {
|
|
39
|
+
const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
|
|
40
|
+
if (!keys.includes(event.key)) return;
|
|
41
|
+
|
|
42
|
+
const tabList = event.currentTarget.closest('[role="tablist"]');
|
|
43
|
+
if (!tabList) return;
|
|
44
|
+
|
|
45
|
+
const tabs = Array.from(
|
|
46
|
+
tabList.querySelectorAll<HTMLButtonElement>('[role="tab"]:not(:disabled)')
|
|
47
|
+
);
|
|
48
|
+
const currentIndex = tabs.indexOf(event.currentTarget);
|
|
49
|
+
if (currentIndex === -1 || tabs.length === 0) return;
|
|
50
|
+
|
|
51
|
+
const nextIndex = event.key === 'Home'
|
|
52
|
+
? 0
|
|
53
|
+
: event.key === 'End'
|
|
54
|
+
? tabs.length - 1
|
|
55
|
+
: event.key === 'ArrowRight' || event.key === 'ArrowDown'
|
|
56
|
+
? (currentIndex + 1) % tabs.length
|
|
57
|
+
: (currentIndex - 1 + tabs.length) % tabs.length;
|
|
58
|
+
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
tabs[nextIndex]?.focus();
|
|
61
|
+
tabs[nextIndex]?.click();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isCompact = $derived(density === 'compact');
|
|
65
|
+
|
|
66
|
+
const toneClasses: Record<string, { activeBadge: string; inactiveBadge: string }> = {
|
|
67
|
+
default: {
|
|
68
|
+
activeBadge: 'bg-primary/20 text-primary',
|
|
69
|
+
inactiveBadge: 'bg-muted text-muted-foreground',
|
|
70
|
+
},
|
|
71
|
+
success: {
|
|
72
|
+
activeBadge: 'bg-success/25 text-success',
|
|
73
|
+
inactiveBadge: 'bg-success/10 text-success',
|
|
74
|
+
},
|
|
75
|
+
warning: {
|
|
76
|
+
activeBadge: 'bg-warning/25 text-warning-foreground',
|
|
77
|
+
inactiveBadge: 'bg-warning/10 text-warning-foreground',
|
|
78
|
+
},
|
|
79
|
+
destructive: {
|
|
80
|
+
activeBadge: 'bg-destructive/25 text-destructive',
|
|
81
|
+
inactiveBadge: 'bg-destructive/10 text-destructive',
|
|
82
|
+
},
|
|
83
|
+
info: {
|
|
84
|
+
activeBadge: 'bg-info/25 text-info',
|
|
85
|
+
inactiveBadge: 'bg-info/10 text-info',
|
|
86
|
+
},
|
|
87
|
+
muted: {
|
|
88
|
+
activeBadge: 'bg-muted text-foreground',
|
|
89
|
+
inactiveBadge: 'bg-muted/50 text-muted-foreground',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div
|
|
95
|
+
role="tablist"
|
|
96
|
+
aria-orientation="horizontal"
|
|
97
|
+
data-svadmin-status-tabs
|
|
98
|
+
data-density={density}
|
|
99
|
+
data-variant={variant}
|
|
100
|
+
class={cn(
|
|
101
|
+
'flex items-center gap-1 overflow-x-auto',
|
|
102
|
+
variant === 'segmented' && 'rounded-lg bg-muted p-1',
|
|
103
|
+
variant === 'underline' && 'border-b border-border gap-2',
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{#each items as item (item.key)}
|
|
108
|
+
{@const active = value === item.key}
|
|
109
|
+
{@const tone = item.tone ?? 'default'}
|
|
110
|
+
{@const badgeTone = toneClasses[tone] ?? toneClasses.default}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
role="tab"
|
|
114
|
+
aria-selected={active}
|
|
115
|
+
tabindex={active ? 0 : -1}
|
|
116
|
+
disabled={item.disabled}
|
|
117
|
+
onclick={() => selectTab(item.key, item.disabled)}
|
|
118
|
+
onkeydown={handleKeydown}
|
|
119
|
+
class={cn(
|
|
120
|
+
'inline-flex items-center gap-1.5 whitespace-nowrap font-medium transition-colors cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50',
|
|
121
|
+
isCompact ? 'text-xs px-2.5 py-1' : 'text-sm px-3 py-1.5',
|
|
122
|
+
variant === 'pills' && [
|
|
123
|
+
'rounded-md border',
|
|
124
|
+
active
|
|
125
|
+
? 'border-border bg-background text-foreground shadow-xs font-semibold'
|
|
126
|
+
: 'border-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground',
|
|
127
|
+
],
|
|
128
|
+
variant === 'segmented' && [
|
|
129
|
+
'rounded-md',
|
|
130
|
+
active
|
|
131
|
+
? 'bg-background text-foreground shadow-xs font-semibold'
|
|
132
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
133
|
+
],
|
|
134
|
+
variant === 'underline' && [
|
|
135
|
+
'-mb-px border-b-2 rounded-none px-3 pb-2 pt-1',
|
|
136
|
+
active
|
|
137
|
+
? 'border-primary text-primary font-semibold'
|
|
138
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground/30',
|
|
139
|
+
]
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<span>{item.label}</span>
|
|
143
|
+
{#if item.count !== undefined}
|
|
144
|
+
<span
|
|
145
|
+
class={cn(
|
|
146
|
+
'inline-flex items-center justify-center rounded-full font-semibold tabular-nums leading-none',
|
|
147
|
+
isCompact ? 'min-w-4 px-1 py-0.5 text-[10px]' : 'min-w-5 px-1.5 py-0.5 text-xs',
|
|
148
|
+
active ? badgeTone.activeBadge : badgeTone.inactiveBadge
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{item.count}
|
|
152
|
+
</span>
|
|
153
|
+
{/if}
|
|
154
|
+
</button>
|
|
155
|
+
{/each}
|
|
156
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StatusTabItem {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
count?: number | string;
|
|
5
|
+
tone?: 'default' | 'success' | 'warning' | 'destructive' | 'info' | 'muted';
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
items: StatusTabItem[];
|
|
10
|
+
value?: string;
|
|
11
|
+
density?: 'compact' | 'comfortable';
|
|
12
|
+
variant?: 'pills' | 'underline' | 'segmented';
|
|
13
|
+
onchange?: (key: string) => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
declare const StatusTabs: import("svelte").Component<Props, {}, "value">;
|
|
17
|
+
type StatusTabs = ReturnType<typeof StatusTabs>;
|
|
18
|
+
export default StatusTabs;
|
|
@@ -12,6 +12,8 @@ export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
|
12
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
13
13
|
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
14
14
|
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
15
|
+
export { default as StatusTabs } from './StatusTabs.svelte';
|
|
16
|
+
export type { StatusTabItem } from './StatusTabs.svelte';
|
|
15
17
|
export { default as FilterToolbar } from './FilterToolbar.svelte';
|
|
16
18
|
export { default as DataState } from './DataState.svelte';
|
|
17
19
|
export type { DataStateKind } from './DataState.svelte';
|
|
@@ -12,6 +12,7 @@ export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
|
12
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
13
13
|
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
14
14
|
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
15
|
+
export { default as StatusTabs } from './StatusTabs.svelte';
|
|
15
16
|
export { default as FilterToolbar } from './FilterToolbar.svelte';
|
|
16
17
|
export { default as DataState } from './DataState.svelte';
|
|
17
18
|
export { default as ProjectCard } from './ProjectCard.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -172,6 +172,8 @@ export { default as DescriptionList } from './components/content/DescriptionList
|
|
|
172
172
|
export type { DescriptionItem } from './components/content/DescriptionList.svelte';
|
|
173
173
|
export { default as StatusBadge } from './components/content/StatusBadge.svelte';
|
|
174
174
|
export type { Status } from './components/content/StatusBadge.svelte';
|
|
175
|
+
export { default as StatusTabs } from './components/content/StatusTabs.svelte';
|
|
176
|
+
export type { StatusTabItem } from './components/content/StatusTabs.svelte';
|
|
175
177
|
export { default as FilterToolbar } from './components/content/FilterToolbar.svelte';
|
|
176
178
|
export { default as DataState } from './components/content/DataState.svelte';
|
|
177
179
|
export type { DataStateKind } from './components/content/DataState.svelte';
|
package/dist/index.js
CHANGED
|
@@ -183,6 +183,7 @@ export { default as SettingsFieldRow } from './components/content/SettingsFieldR
|
|
|
183
183
|
export { default as MetricBlock } from './components/content/MetricBlock.svelte';
|
|
184
184
|
export { default as DescriptionList } from './components/content/DescriptionList.svelte';
|
|
185
185
|
export { default as StatusBadge } from './components/content/StatusBadge.svelte';
|
|
186
|
+
export { default as StatusTabs } from './components/content/StatusTabs.svelte';
|
|
186
187
|
export { default as FilterToolbar } from './components/content/FilterToolbar.svelte';
|
|
187
188
|
export { default as DataState } from './components/content/DataState.svelte';
|
|
188
189
|
export { default as ProjectCard } from './components/content/ProjectCard.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./components/*": "./dist/components/*"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@svadmin/core": "^0.
|
|
42
|
+
"@svadmin/core": "^0.41.0",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|