minka-ds 0.3.3 → 0.3.4
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/package.json +1 -1
- package/src/components/ui/stat-card.tsx +179 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
import { Badge } from "./badge"
|
|
6
|
+
import { Button } from "./button"
|
|
7
|
+
|
|
8
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type StatCardColor = "default" | "success" | "error" | "muted"
|
|
11
|
+
|
|
12
|
+
export interface StatCardAction {
|
|
13
|
+
label: string
|
|
14
|
+
icon?: React.ReactNode
|
|
15
|
+
onClick: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface StatCardBase {
|
|
19
|
+
label: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StatCardCountProps extends StatCardBase {
|
|
24
|
+
type?: "count"
|
|
25
|
+
value: number | null
|
|
26
|
+
percent?: number
|
|
27
|
+
color?: StatCardColor
|
|
28
|
+
dot?: string
|
|
29
|
+
onClick?: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StatCardAmountProps extends StatCardBase {
|
|
33
|
+
type: "amount"
|
|
34
|
+
value: string | number | null
|
|
35
|
+
unit?: string
|
|
36
|
+
percent?: number
|
|
37
|
+
color?: StatCardColor
|
|
38
|
+
secondary?: string
|
|
39
|
+
badge?: string
|
|
40
|
+
actions?: StatCardAction[]
|
|
41
|
+
onClick?: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StatCardStatusProps extends StatCardBase {
|
|
45
|
+
type: "status"
|
|
46
|
+
status: string
|
|
47
|
+
color: "success" | "error" | "muted"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type StatCardProps =
|
|
51
|
+
| StatCardCountProps
|
|
52
|
+
| StatCardAmountProps
|
|
53
|
+
| StatCardStatusProps
|
|
54
|
+
|
|
55
|
+
// ── Color maps ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const VALUE_COLOR: Record<StatCardColor, string> = {
|
|
58
|
+
default: "text-[var(--color-text-default)]",
|
|
59
|
+
success: "text-[var(--color-feedback-success)]",
|
|
60
|
+
error: "text-[var(--color-feedback-error)]",
|
|
61
|
+
muted: "text-[var(--color-text-muted)]",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const STATUS_TEXT_COLOR: Record<"success" | "error" | "muted", string> = {
|
|
65
|
+
success: "text-[var(--color-feedback-success)]",
|
|
66
|
+
error: "text-[var(--color-feedback-error)]",
|
|
67
|
+
muted: "text-[var(--color-text-disabled)]",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const STATUS_DOT_COLOR: Record<"success" | "error" | "muted", string> = {
|
|
71
|
+
success: "bg-[var(--color-feedback-success)]",
|
|
72
|
+
error: "bg-[var(--color-feedback-error)]",
|
|
73
|
+
muted: "bg-[var(--color-text-disabled)]",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const CARD_BASE = "[border-radius:var(--radius-card)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] px-4 py-3"
|
|
77
|
+
|
|
78
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function StatCard(props: StatCardProps) {
|
|
81
|
+
// Count
|
|
82
|
+
if (!props.type || props.type === "count") {
|
|
83
|
+
const { label, value, percent, color = "default", dot, onClick, className } = props as StatCardCountProps
|
|
84
|
+
const Comp = onClick ? "button" : "div"
|
|
85
|
+
return (
|
|
86
|
+
<Comp
|
|
87
|
+
onClick={onClick}
|
|
88
|
+
className={cn(
|
|
89
|
+
"flex flex-col gap-1",
|
|
90
|
+
CARD_BASE,
|
|
91
|
+
onClick && "text-left hover:bg-[var(--color-bg-table-hover)] transition-colors cursor-pointer",
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center gap-1.5">
|
|
96
|
+
{dot && <span className="size-1.5 rounded-full shrink-0" style={{ background: dot }} />}
|
|
97
|
+
<span className="text-body-sm-light text-[var(--color-text-default)]">{label}</span>
|
|
98
|
+
{percent !== undefined && (
|
|
99
|
+
<span className="text-caption-sm text-[var(--color-text-hint)]">{percent}%</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{value === null ? (
|
|
103
|
+
<span className="text-heading-2-serif text-[var(--color-text-disabled)]">—</span>
|
|
104
|
+
) : (
|
|
105
|
+
<span className={cn("text-heading-2-serif tabular-nums", VALUE_COLOR[color])}>
|
|
106
|
+
{typeof value === "number" ? value.toLocaleString("en-US") : value}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</Comp>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Amount
|
|
114
|
+
if (props.type === "amount") {
|
|
115
|
+
const { label, value, unit, percent, color = "default", secondary, badge, actions, onClick, className } = props
|
|
116
|
+
const Comp = onClick ? "button" : "div"
|
|
117
|
+
return (
|
|
118
|
+
<Comp
|
|
119
|
+
onClick={onClick}
|
|
120
|
+
className={cn(
|
|
121
|
+
"flex flex-col gap-1",
|
|
122
|
+
CARD_BASE,
|
|
123
|
+
onClick && "text-left hover:bg-[var(--color-bg-table-hover)] transition-colors cursor-pointer",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<span className="text-body-sm-light text-[var(--color-text-muted)]">{label}</span>
|
|
129
|
+
{percent !== undefined && (
|
|
130
|
+
<span className="text-caption-sm text-[var(--color-text-hint)]">{percent}%</span>
|
|
131
|
+
)}
|
|
132
|
+
{secondary && <span className="text-caption text-[var(--color-text-muted)]">{secondary}</span>}
|
|
133
|
+
{badge && <Badge variant="info">{badge}</Badge>}
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex items-center justify-between gap-3">
|
|
136
|
+
<div className="flex items-baseline gap-1.5">
|
|
137
|
+
{value === null ? (
|
|
138
|
+
<span className="text-heading-1-serif text-[var(--color-text-disabled)]">—</span>
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
<span className={cn("text-heading-2-serif tabular-nums tracking-tight", VALUE_COLOR[color])}>
|
|
142
|
+
{value}
|
|
143
|
+
</span>
|
|
144
|
+
{unit && <span className="text-caption text-[var(--color-text-muted)]">{unit}</span>}
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
{actions && actions.length > 0 && (
|
|
149
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
150
|
+
{actions.map((action, i) => (
|
|
151
|
+
<Button key={i} variant="outline" size="sm" onClick={action.onClick}>
|
|
152
|
+
{action.icon}{action.label}
|
|
153
|
+
</Button>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</Comp>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Status
|
|
163
|
+
if (props.type === "status") {
|
|
164
|
+
const { label, status, color, className } = props
|
|
165
|
+
return (
|
|
166
|
+
<div className={cn("flex flex-col gap-1", CARD_BASE, className)}>
|
|
167
|
+
<span className="text-body-sm-light text-[var(--color-text-muted)]">{label}</span>
|
|
168
|
+
<span className={cn("inline-flex items-center gap-1.5 text-heading-2-serif", STATUS_TEXT_COLOR[color])}>
|
|
169
|
+
<span className={cn("size-2 rounded-full shrink-0", STATUS_DOT_COLOR[color])} />
|
|
170
|
+
{status}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { StatCard }
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from "./components/ui/data-table"
|
|
|
21
21
|
export * from "./components/ui/dialog"
|
|
22
22
|
export * from "./components/ui/dropdown-menu"
|
|
23
23
|
export * from "./components/ui/filter-chip"
|
|
24
|
+
export * from "./components/ui/stat-card"
|
|
24
25
|
export * from "./components/ui/filter-combobox"
|
|
25
26
|
export * from "./components/ui/search-bar"
|
|
26
27
|
export * from "./components/ui/input"
|
package/src/lib/utils.ts
CHANGED