@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
@@ -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 { resource, recordItemId, hideText = false, accessControl = { enabled: true, hideIfUnauthorized: true }, class: className = '' } = $props<{
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={() => nav.show(resource, recordItemId)}
56
+ onclick={navigateToRecord}
37
57
  >
38
58
  <Eye class="h-4 w-4" />
39
- {#if !hideText}<span class="ml-1">{i18n.t('common.detail')}</span>{/if}
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: string | number;
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
- let { items = [], columns = 1, class: className = '' }: Props = $props();
14
- const columnClass = $derived(columns === 2 ? 'sm:grid-cols-2' : 'grid-cols-1');
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
- <dl class={'grid gap-x-6 gap-y-5 ' + columnClass + ' ' + className}>
18
- {#each items as item (item.label)}
19
- <div class="min-w-0">
20
- <dt class="text-xs font-medium text-muted-foreground">{item.label}</dt>
21
- <dd class="mt-1 break-words text-sm font-medium text-foreground">
22
- {#if item.href}<a class="text-primary underline-offset-4 hover:underline" href={item.href}>{item.value}</a>{:else}{item.value}{/if}
23
- </dd>
24
- {#if item.description}<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>{/if}
25
- </div>
26
- {/each}
27
- </dl>
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: string | number;
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={'flex flex-col gap-2 sm:flex-row sm:items-center ' + className}>
25
- <div class="relative min-w-0 flex-1">
26
- <Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
27
- <Input bind:value={query} {placeholder} class="pl-9 pr-9" aria-label={placeholder} />
28
- {#if query}<Button variant="ghost" size="icon-xs" class="absolute right-1 top-1/2 -translate-y-1/2" aria-label={clearLabel} title={clearLabel} onclick={() => query = ''}><X class="size-3.5" /></Button>{/if}
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 filters}<div class="flex flex-wrap items-center gap-2">{@render filters()}</div>{/if}
31
- {#if actions}<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>{/if}
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.49.3",
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.40.1",
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",