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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minka-ds",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Minka product design system — tokenized component library",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -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
@@ -21,6 +21,9 @@ const twMerge = extendTailwindMerge({
21
21
  "caption-light", "caption-sm-light",
22
22
  "overline",
23
23
  "code",
24
+ "heading-1-serif", "heading-2-serif", "heading-3-serif", "heading-4-serif",
25
+ "display-serif",
26
+ "caption-lg-serif",
24
27
  ],
25
28
  },
26
29
  ],