@svadmin/ui 0.65.1 → 0.66.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.
@@ -0,0 +1,149 @@
1
+ <script lang="ts">
2
+ import { Skeleton } from './ui/skeleton/index.js';
3
+ import { cn } from '../utils.js';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ export type MetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
7
+
8
+ export interface MetricStripItem {
9
+ id?: string;
10
+ label: string;
11
+ value: string | number;
12
+ tone?: MetricTone;
13
+ badge?: {
14
+ text: string;
15
+ tone?: 'default' | 'success' | 'warning' | 'danger' | 'info';
16
+ };
17
+ icon?: import('svelte').Component<{ class?: string }>;
18
+ href?: string;
19
+ trend?: {
20
+ value: number;
21
+ label?: string;
22
+ };
23
+ loading?: boolean;
24
+ class?: string;
25
+ }
26
+
27
+ interface Props {
28
+ items?: MetricStripItem[];
29
+ columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
30
+ ariaLabel?: string;
31
+ class?: string;
32
+ children?: Snippet;
33
+ }
34
+
35
+ let {
36
+ items = [],
37
+ columns = 'auto',
38
+ ariaLabel = 'Metrics overview',
39
+ class: className = '',
40
+ children,
41
+ }: Props = $props();
42
+
43
+ const columnClassMap = {
44
+ 2: 'grid-cols-1 sm:grid-cols-2',
45
+ 3: 'grid-cols-1 sm:grid-cols-3',
46
+ 4: 'grid-cols-2 sm:grid-cols-4',
47
+ 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
48
+ 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
49
+ auto: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
50
+ };
51
+
52
+ const toneValueColorMap: Record<MetricTone, string> = {
53
+ default: 'text-foreground',
54
+ primary: 'text-primary',
55
+ success: 'text-success',
56
+ warning: 'text-warning',
57
+ danger: 'text-destructive',
58
+ info: 'text-info',
59
+ };
60
+
61
+ const badgeToneMap: Record<
62
+ NonNullable<NonNullable<MetricStripItem['badge']>['tone']>,
63
+ string
64
+ > = {
65
+ default: 'bg-muted text-muted-foreground border-border',
66
+ success: 'bg-success/10 text-success border-success/20',
67
+ warning: 'bg-warning/10 text-warning border-warning/20',
68
+ danger: 'bg-destructive/10 text-destructive border-destructive/20',
69
+ info: 'bg-info/10 text-info border-info/20',
70
+ };
71
+ </script>
72
+
73
+ <div
74
+ role="region"
75
+ aria-label={ariaLabel}
76
+ class={cn(
77
+ 'grid gap-px overflow-hidden rounded-lg border border-border bg-border shadow-xs',
78
+ columnClassMap[columns] || columnClassMap.auto,
79
+ className
80
+ )}
81
+ >
82
+ {#if children}
83
+ {@render children()}
84
+ {:else}
85
+ {#each items as item, index (item.id || index)}
86
+ {@const isInteractive = Boolean(item.href)}
87
+ <svelte:element
88
+ this={isInteractive ? 'a' : 'div'}
89
+ href={item.href}
90
+ class={cn(
91
+ 'group relative flex flex-col justify-between gap-1 bg-card p-3.5 text-left transition-colors duration-150 sm:p-4',
92
+ isInteractive && 'cursor-pointer text-inherit no-underline hover:bg-muted/40',
93
+ item.class
94
+ )}
95
+ >
96
+ <div class="flex items-center justify-between gap-2">
97
+ <span class="truncate text-xs font-semibold text-muted-foreground">{item.label}</span>
98
+ {#if item.icon}
99
+ <span class="shrink-0 text-muted-foreground/70">
100
+ <item.icon class="h-3.5 w-3.5" />
101
+ </span>
102
+ {/if}
103
+ </div>
104
+
105
+ {#if item.loading}
106
+ <div class="mt-1 flex items-center justify-between">
107
+ <Skeleton class="h-6 w-16" />
108
+ </div>
109
+ {:else}
110
+ <div class="mt-0.5 flex items-baseline justify-between gap-2">
111
+ <strong
112
+ class={cn(
113
+ 'text-xl font-bold tracking-tight tabular-nums sm:text-2xl',
114
+ toneValueColorMap[item.tone || 'default']
115
+ )}
116
+ >
117
+ {item.value}
118
+ </strong>
119
+
120
+ {#if item.badge}
121
+ <span
122
+ class={cn(
123
+ 'inline-flex items-center rounded-full border px-1.5 py-0.5 text-[11px] font-semibold',
124
+ badgeToneMap[item.badge.tone || 'default']
125
+ )}
126
+ >
127
+ {item.badge.text}
128
+ </span>
129
+ {:else if item.trend}
130
+ <span
131
+ class={cn(
132
+ 'inline-flex items-center gap-0.5 text-xs font-medium tabular-nums',
133
+ item.trend.value >= 0 ? 'text-success' : 'text-destructive'
134
+ )}
135
+ aria-label="{item.trend.value >= 0 ? '上升' : '下降'} {Math.abs(item.trend.value)}%"
136
+ >
137
+ <span aria-hidden="true">{item.trend.value >= 0 ? '↑' : '↓'}</span>
138
+ <span>{Math.abs(item.trend.value)}%</span>
139
+ {#if item.trend.label}
140
+ <span class="ml-0.5 text-muted-foreground">{item.trend.label}</span>
141
+ {/if}
142
+ </span>
143
+ {/if}
144
+ </div>
145
+ {/if}
146
+ </svelte:element>
147
+ {/each}
148
+ {/if}
149
+ </div>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type MetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
3
+ export interface MetricStripItem {
4
+ id?: string;
5
+ label: string;
6
+ value: string | number;
7
+ tone?: MetricTone;
8
+ badge?: {
9
+ text: string;
10
+ tone?: 'default' | 'success' | 'warning' | 'danger' | 'info';
11
+ };
12
+ icon?: import('svelte').Component<{
13
+ class?: string;
14
+ }>;
15
+ href?: string;
16
+ trend?: {
17
+ value: number;
18
+ label?: string;
19
+ };
20
+ loading?: boolean;
21
+ class?: string;
22
+ }
23
+ interface Props {
24
+ items?: MetricStripItem[];
25
+ columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
26
+ ariaLabel?: string;
27
+ class?: string;
28
+ children?: Snippet;
29
+ }
30
+ declare const MetricStrip: import("svelte").Component<Props, {}, "">;
31
+ type MetricStrip = ReturnType<typeof MetricStrip>;
32
+ export default MetricStrip;
@@ -8,6 +8,11 @@
8
8
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
9
9
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
10
10
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
11
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15",
12
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15",
13
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15",
14
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15",
15
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium",
11
16
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
12
17
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
13
18
  link: "text-primary underline-offset-4 hover:underline",
@@ -4,6 +4,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
4
4
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
5
5
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
6
6
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
7
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
8
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
9
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
10
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
11
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
7
12
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
8
13
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
9
14
  link: "text-primary underline-offset-4 hover:underline";
@@ -13,6 +18,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
13
18
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
14
19
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
15
20
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
21
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
22
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
23
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
24
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
25
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
16
26
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
17
27
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
18
28
  link: "text-primary underline-offset-4 hover:underline";
@@ -22,6 +32,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
22
32
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
23
33
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
24
34
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
35
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
36
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
37
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
38
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
39
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
25
40
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
26
41
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
27
42
  link: "text-primary underline-offset-4 hover:underline";
@@ -4,19 +4,31 @@
4
4
  import type { HTMLTdAttributes } from "svelte/elements";
5
5
  import type { TableDensity } from "./table.svelte";
6
6
 
7
+ type Props = WithElementRef<HTMLTdAttributes, HTMLTableCellElement> & {
8
+ sticky?: "left" | "right" | boolean;
9
+ };
10
+
7
11
  let {
8
12
  ref = $bindable(null),
13
+ sticky = false,
9
14
  class: className,
10
15
  children,
11
16
  ...restProps
12
- }: WithElementRef<HTMLTdAttributes, HTMLTableCellElement> = $props();
17
+ }: Props = $props();
13
18
 
14
19
  const getDensity = getContext<() => TableDensity>("svadmin-table-density");
15
20
  const densityClass = $derived(getDensity?.() === "compact"
16
21
  ? "h-8 px-2 py-1 text-xs"
17
22
  : "p-2");
23
+ const stickyClass = $derived(
24
+ sticky === "right" || sticky === true
25
+ ? "sticky right-0 z-10 bg-background shadow-[-1px_0_0_var(--border)] group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted"
26
+ : sticky === "left"
27
+ ? "sticky left-0 z-10 bg-background shadow-[1px_0_0_var(--border)] group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted"
28
+ : ""
29
+ );
18
30
  </script>
19
31
 
20
- <td bind:this={ref} data-slot="table-cell" class={cn("align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
32
+ <td bind:this={ref} data-slot="table-cell" data-sticky={sticky ? (typeof sticky === "string" ? sticky : "right") : undefined} class={cn("align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, stickyClass, className)} {...restProps}>
21
33
  {@render children?.()}
22
34
  </td>
@@ -1,5 +1,8 @@
1
1
  import { type WithElementRef } from "../../../utils.js";
2
2
  import type { HTMLTdAttributes } from "svelte/elements";
3
- declare const TableCell: import("svelte").Component<WithElementRef<HTMLTdAttributes, HTMLTableCellElement>, {}, "ref">;
3
+ type Props = WithElementRef<HTMLTdAttributes, HTMLTableCellElement> & {
4
+ sticky?: "left" | "right" | boolean;
5
+ };
6
+ declare const TableCell: import("svelte").Component<Props, {}, "ref">;
4
7
  type TableCell = ReturnType<typeof TableCell>;
5
8
  export default TableCell;
@@ -4,19 +4,31 @@
4
4
  import type { HTMLThAttributes } from "svelte/elements";
5
5
  import type { TableDensity } from "./table.svelte";
6
6
 
7
+ type Props = WithElementRef<HTMLThAttributes, HTMLTableCellElement> & {
8
+ sticky?: "left" | "right" | boolean;
9
+ };
10
+
7
11
  let {
8
12
  ref = $bindable(null),
13
+ sticky = false,
9
14
  class: className,
10
15
  children,
11
16
  ...restProps
12
- }: WithElementRef<HTMLThAttributes, HTMLTableCellElement> = $props();
17
+ }: Props = $props();
13
18
 
14
19
  const getDensity = getContext<() => TableDensity>("svadmin-table-density");
15
20
  const densityClass = $derived(getDensity?.() === "compact"
16
21
  ? "h-8 px-2 py-1 text-xs"
17
22
  : "h-9 px-2");
23
+ const stickyClass = $derived(
24
+ sticky === "right" || sticky === true
25
+ ? "sticky right-0 z-20 bg-background shadow-[-1px_0_0_var(--border)]"
26
+ : sticky === "left"
27
+ ? "sticky left-0 z-20 bg-background shadow-[1px_0_0_var(--border)]"
28
+ : ""
29
+ );
18
30
  </script>
19
31
 
20
- <th bind:this={ref} data-slot="table-head" class={cn("text-foreground text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
32
+ <th bind:this={ref} data-slot="table-head" data-sticky={sticky ? (typeof sticky === "string" ? sticky : "right") : undefined} class={cn("text-foreground text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, stickyClass, className)} {...restProps}>
21
33
  {@render children?.()}
22
34
  </th>
@@ -1,5 +1,8 @@
1
1
  import { type WithElementRef } from "../../../utils.js";
2
2
  import type { HTMLThAttributes } from "svelte/elements";
3
- declare const TableHead: import("svelte").Component<WithElementRef<HTMLThAttributes, HTMLTableCellElement>, {}, "ref">;
3
+ type Props = WithElementRef<HTMLThAttributes, HTMLTableCellElement> & {
4
+ sticky?: "left" | "right" | boolean;
5
+ };
6
+ declare const TableHead: import("svelte").Component<Props, {}, "ref">;
4
7
  type TableHead = ReturnType<typeof TableHead>;
5
8
  export default TableHead;
@@ -10,6 +10,6 @@
10
10
  }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
11
11
  </script>
12
12
 
13
- <tr bind:this={ref} data-slot="table-row" class={cn("hover:bg-muted/50 data-[state=selected]:bg-muted transition-colors", className)} {...restProps}>
13
+ <tr bind:this={ref} data-slot="table-row" class={cn("group/row hover:bg-muted/50 data-[state=selected]:bg-muted transition-colors", className)} {...restProps}>
14
14
  {@render children?.()}
15
15
  </tr>
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
13
13
  export { default as FieldRenderer } from './components/FieldRenderer.svelte';
14
14
  export { default as EmptyState } from './components/EmptyState.svelte';
15
15
  export { default as StatsCard } from './components/StatsCard.svelte';
16
+ export { default as MetricStrip } from './components/MetricStrip.svelte';
17
+ export type { MetricStripItem, MetricTone } from './components/MetricStrip.svelte';
16
18
  export { default as PageHeader } from './components/PageHeader.svelte';
17
19
  export { default as ResourceOperationsPage } from './components/ResourceOperationsPage.svelte';
18
20
  export { default as RecordDetailDrawer } from './components/RecordDetailDrawer.svelte';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
16
16
  export { default as FieldRenderer } from './components/FieldRenderer.svelte';
17
17
  export { default as EmptyState } from './components/EmptyState.svelte';
18
18
  export { default as StatsCard } from './components/StatsCard.svelte';
19
+ export { default as MetricStrip } from './components/MetricStrip.svelte';
19
20
  export { default as PageHeader } from './components/PageHeader.svelte';
20
21
  export { default as ResourceOperationsPage } from './components/ResourceOperationsPage.svelte';
21
22
  export { default as RecordDetailDrawer } from './components/RecordDetailDrawer.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.65.1",
3
+ "version": "0.66.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [