@stampui/blocks 1.1.1 → 2.0.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/manifests.js +917 -170
- package/package.json +15 -10
- package/src/components/blocks/animated-counter.tsx +70 -0
- package/src/components/blocks/gradient-text.tsx +39 -0
- package/src/components/blocks/grid-wave.tsx +40 -0
- package/src/components/blocks/loading-card.tsx +48 -0
- package/src/components/blocks/loading-dots.tsx +68 -0
- package/src/components/blocks/orbit-trail.tsx +30 -0
- package/src/components/blocks/progress-ring.tsx +72 -0
- package/src/components/blocks/registry-card.tsx +6 -7
- package/src/components/blocks/signal-arc.tsx +32 -0
- package/src/components/blocks/typewriter-text.tsx +62 -0
- package/src/components/core/alert-dialog.tsx +2 -2
- package/src/components/core/avatar.tsx +8 -4
- package/src/components/core/button.tsx +1 -1
- package/src/components/core/checkbox.tsx +1 -1
- package/src/components/core/combobox.tsx +1 -1
- package/src/components/core/command.tsx +7 -4
- package/src/components/core/date-picker.tsx +1 -1
- package/src/components/core/dialog.tsx +1 -1
- package/src/components/core/drawer.tsx +1 -1
- package/src/components/core/input.tsx +2 -0
- package/src/components/core/label.tsx +1 -1
- package/src/components/core/multi-select.tsx +1 -1
- package/src/components/core/native-select.tsx +1 -1
- package/src/components/core/password-input.tsx +3 -0
- package/src/components/core/radio-group.tsx +1 -1
- package/src/components/core/resizable.tsx +1 -1
- package/src/components/core/select.tsx +1 -1
- package/src/components/core/sheet.tsx +1 -1
- package/src/components/core/slider.tsx +1 -1
- package/src/components/core/status-pulse.tsx +6 -0
- package/src/components/core/switch.tsx +1 -1
- package/src/components/core/table.tsx +7 -2
- package/src/components/core/tabs.tsx +1 -1
- package/src/components/core/toggle.tsx +1 -1
- package/src/components/core/typing-indicator.tsx +41 -27
- package/src/manifests.ts +932 -183
- package/src/components/blocks/ai-chat-shell.tsx +0 -97
- package/src/components/blocks/auth-panel.tsx +0 -203
- package/src/components/blocks/dashboard-shell.tsx +0 -135
- package/src/components/blocks/notification-center.tsx +0 -185
- package/src/components/blocks/onboarding-flow.tsx +0 -230
- package/src/components/blocks/project-command-center.tsx +0 -188
- package/src/components/blocks/prompt-input.tsx +0 -81
- package/src/components/blocks/settings-layout.tsx +0 -178
- package/src/components/blocks/token-stream.tsx +0 -42
- package/src/components/core/carousel.tsx +0 -170
- package/src/components/core/chart.tsx +0 -377
- package/src/components/core/data-table.tsx +0 -173
- package/src/components/core/file-upload.tsx +0 -143
- package/src/components/core/input-otp.tsx +0 -108
- package/src/components/core/stepper.tsx +0 -111
- package/src/components/core/timeline.tsx +0 -81
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cx } from "@/lib/cx"
|
|
5
|
-
|
|
6
|
-
export interface TokenStreamProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
-
content: string
|
|
8
|
-
speed?: number
|
|
9
|
-
onComplete?: () => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function TokenStream({
|
|
13
|
-
content,
|
|
14
|
-
speed = 15,
|
|
15
|
-
onComplete,
|
|
16
|
-
className,
|
|
17
|
-
...props
|
|
18
|
-
}: TokenStreamProps) {
|
|
19
|
-
const [displayedContent, setDisplayedContent] = React.useState("")
|
|
20
|
-
const [currentIndex, setCurrentIndex] = React.useState(0)
|
|
21
|
-
|
|
22
|
-
React.useEffect(() => {
|
|
23
|
-
if (currentIndex < content.length) {
|
|
24
|
-
const timeout = setTimeout(() => {
|
|
25
|
-
setDisplayedContent((prev) => prev + content[currentIndex])
|
|
26
|
-
setCurrentIndex((prev) => prev + 1)
|
|
27
|
-
}, speed)
|
|
28
|
-
return () => clearTimeout(timeout)
|
|
29
|
-
} else if (onComplete) {
|
|
30
|
-
onComplete()
|
|
31
|
-
}
|
|
32
|
-
}, [content, currentIndex, speed, onComplete])
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className={cx("relative inline-block", className)} {...props}>
|
|
36
|
-
<span className="whitespace-pre-wrap">{displayedContent}</span>
|
|
37
|
-
{currentIndex < content.length && (
|
|
38
|
-
<span className="ml-1 inline-block h-4 w-1 animate-pulse bg-primary align-middle" />
|
|
39
|
-
)}
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
5
|
-
import { cx } from "@/lib/cx"
|
|
6
|
-
|
|
7
|
-
// ── Context ───────────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
interface CarouselContextValue {
|
|
10
|
-
current: number
|
|
11
|
-
count: number
|
|
12
|
-
setCount: (n: number) => void
|
|
13
|
-
canPrev: boolean
|
|
14
|
-
canNext: boolean
|
|
15
|
-
prev: () => void
|
|
16
|
-
next: () => void
|
|
17
|
-
goTo: (index: number) => void
|
|
18
|
-
loop: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
|
22
|
-
|
|
23
|
-
function useCarousel() {
|
|
24
|
-
const ctx = React.useContext(CarouselContext)
|
|
25
|
-
if (!ctx) throw new Error("Must be inside Carousel")
|
|
26
|
-
return ctx
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ── Root ──────────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export interface CarouselProps {
|
|
32
|
-
children: React.ReactNode
|
|
33
|
-
loop?: boolean
|
|
34
|
-
autoPlay?: number
|
|
35
|
-
className?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function Carousel({ children, loop = false, autoPlay, className }: CarouselProps) {
|
|
39
|
-
const [current, setCurrent] = React.useState(0)
|
|
40
|
-
const [count, setCount] = React.useState(0)
|
|
41
|
-
|
|
42
|
-
const canPrev = loop || current > 0
|
|
43
|
-
const canNext = loop || current < count - 1
|
|
44
|
-
|
|
45
|
-
function prev() {
|
|
46
|
-
setCurrent((c) => loop ? (c - 1 + count) % count : Math.max(0, c - 1))
|
|
47
|
-
}
|
|
48
|
-
function next() {
|
|
49
|
-
setCurrent((c) => loop ? (c + 1) % count : Math.min(count - 1, c + 1))
|
|
50
|
-
}
|
|
51
|
-
function goTo(index: number) {
|
|
52
|
-
setCurrent(Math.max(0, Math.min(count - 1, index)))
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
React.useEffect(() => {
|
|
56
|
-
if (!autoPlay || count === 0) return
|
|
57
|
-
const t = setInterval(next, autoPlay)
|
|
58
|
-
return () => clearInterval(t)
|
|
59
|
-
}, [autoPlay, count, current])
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<CarouselContext.Provider value={{ current, count, setCount, canPrev, canNext, prev, next, goTo, loop }}>
|
|
63
|
-
<div className={cx("relative", className)} data-carousel>
|
|
64
|
-
{children}
|
|
65
|
-
</div>
|
|
66
|
-
</CarouselContext.Provider>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── Content ───────────────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
export function CarouselContent({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
73
|
-
const { current, setCount } = useCarousel()
|
|
74
|
-
const items = React.Children.toArray(children)
|
|
75
|
-
|
|
76
|
-
React.useEffect(() => {
|
|
77
|
-
setCount(items.length)
|
|
78
|
-
}, [items.length])
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div className={cx("overflow-hidden", className)}>
|
|
82
|
-
<div
|
|
83
|
-
data-carousel-content
|
|
84
|
-
className="flex transition-transform duration-300 ease-in-out"
|
|
85
|
-
style={{ transform: `translateX(-${current * 100}%)` }}
|
|
86
|
-
>
|
|
87
|
-
{items.map((child, i) => (
|
|
88
|
-
<div key={i} className="w-full shrink-0">
|
|
89
|
-
{child}
|
|
90
|
-
</div>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ── Item ──────────────────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
export function CarouselItem({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
100
|
-
return <div className={cx("w-full", className)}>{children}</div>
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Controls ──────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
export function CarouselPrevious({ className }: { className?: string }) {
|
|
106
|
-
const { prev, canPrev } = useCarousel()
|
|
107
|
-
return (
|
|
108
|
-
<button
|
|
109
|
-
onClick={prev}
|
|
110
|
-
disabled={!canPrev}
|
|
111
|
-
className={cx(
|
|
112
|
-
"flex h-8 w-8 items-center justify-center rounded-full border border-border bg-card shadow-sm transition-colors",
|
|
113
|
-
"hover:bg-surface-2 disabled:opacity-40 disabled:pointer-events-none",
|
|
114
|
-
className
|
|
115
|
-
)}
|
|
116
|
-
>
|
|
117
|
-
<ChevronLeft className="h-4 w-4" />
|
|
118
|
-
<span className="sr-only">Previous</span>
|
|
119
|
-
</button>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function CarouselNext({ className }: { className?: string }) {
|
|
124
|
-
const { next, canNext } = useCarousel()
|
|
125
|
-
return (
|
|
126
|
-
<button
|
|
127
|
-
onClick={next}
|
|
128
|
-
disabled={!canNext}
|
|
129
|
-
className={cx(
|
|
130
|
-
"flex h-8 w-8 items-center justify-center rounded-full border border-border bg-card shadow-sm transition-colors",
|
|
131
|
-
"hover:bg-surface-2 disabled:opacity-40 disabled:pointer-events-none",
|
|
132
|
-
className
|
|
133
|
-
)}
|
|
134
|
-
>
|
|
135
|
-
<ChevronRight className="h-4 w-4" />
|
|
136
|
-
<span className="sr-only">Next</span>
|
|
137
|
-
</button>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── Dots ──────────────────────────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
export function CarouselDots({ className }: { className?: string }) {
|
|
144
|
-
const { current, count, goTo } = useCarousel()
|
|
145
|
-
return (
|
|
146
|
-
<div className={cx("flex items-center gap-1.5", className)}>
|
|
147
|
-
{Array.from({ length: count }).map((_, i) => (
|
|
148
|
-
<button
|
|
149
|
-
key={i}
|
|
150
|
-
onClick={() => goTo(i)}
|
|
151
|
-
className={cx(
|
|
152
|
-
"h-1.5 rounded-full transition-all duration-200",
|
|
153
|
-
i === current ? "w-4 bg-foreground" : "w-1.5 bg-border-strong hover:bg-muted-foreground"
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
))}
|
|
157
|
-
</div>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Counter ───────────────────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
export function CarouselCounter({ className }: { className?: string }) {
|
|
164
|
-
const { current, count } = useCarousel()
|
|
165
|
-
return (
|
|
166
|
-
<span className={cx("text-xs text-muted-foreground", className)}>
|
|
167
|
-
{current + 1} / {count}
|
|
168
|
-
</span>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import {
|
|
5
|
-
LineChart as ReLineChart,
|
|
6
|
-
Line,
|
|
7
|
-
BarChart as ReBarChart,
|
|
8
|
-
Bar,
|
|
9
|
-
AreaChart as ReAreaChart,
|
|
10
|
-
Area,
|
|
11
|
-
PieChart as RePieChart,
|
|
12
|
-
Pie,
|
|
13
|
-
Cell,
|
|
14
|
-
XAxis,
|
|
15
|
-
YAxis,
|
|
16
|
-
CartesianGrid,
|
|
17
|
-
Tooltip,
|
|
18
|
-
Legend,
|
|
19
|
-
ResponsiveContainer,
|
|
20
|
-
} from "recharts"
|
|
21
|
-
import { cx } from "@/lib/cx"
|
|
22
|
-
|
|
23
|
-
// ── Design tokens ─────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export const CHART_COLORS = [
|
|
26
|
-
"var(--color-foreground)",
|
|
27
|
-
"var(--color-muted-foreground)",
|
|
28
|
-
"var(--color-border-strong)",
|
|
29
|
-
"var(--color-border)",
|
|
30
|
-
"var(--color-surface-2)",
|
|
31
|
-
] as const
|
|
32
|
-
|
|
33
|
-
// Explicit palette for charts that need distinct hues
|
|
34
|
-
export const PALETTE = [
|
|
35
|
-
"#18181b", // zinc-900
|
|
36
|
-
"#71717a", // zinc-500
|
|
37
|
-
"#a1a1aa", // zinc-400
|
|
38
|
-
"#d4d4d8", // zinc-300
|
|
39
|
-
"#e4e4e7", // zinc-200
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
// ── Tooltip ───────────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
interface CustomTooltipProps {
|
|
45
|
-
active?: boolean
|
|
46
|
-
payload?: Array<{ name: string; value: number; color: string }>
|
|
47
|
-
label?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
|
|
51
|
-
if (!active || !payload?.length) return null
|
|
52
|
-
return (
|
|
53
|
-
<div className="rounded-xl border border-border bg-card px-3 py-2.5 shadow-lg text-xs">
|
|
54
|
-
{label && <p className="mb-1.5 font-medium text-foreground">{label}</p>}
|
|
55
|
-
{payload.map((entry, i) => (
|
|
56
|
-
<div key={i} className="flex items-center gap-2 text-muted-foreground">
|
|
57
|
-
<span className="h-1.5 w-1.5 rounded-full shrink-0" style={{ backgroundColor: entry.color }} />
|
|
58
|
-
<span>{entry.name}:</span>
|
|
59
|
-
<span className="font-medium text-foreground">{entry.value}</span>
|
|
60
|
-
</div>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Shared axis props ─────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
const AXIS_STYLE = {
|
|
69
|
-
tick: { fontSize: 11, fill: "var(--color-muted-foreground)" },
|
|
70
|
-
axisLine: false as const,
|
|
71
|
-
tickLine: false as const,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── LineChart ─────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
export interface LineChartSeries {
|
|
77
|
-
key: string
|
|
78
|
-
label?: string
|
|
79
|
-
color?: string
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface LineChartProps {
|
|
83
|
-
data: Record<string, unknown>[]
|
|
84
|
-
xKey: string
|
|
85
|
-
series: LineChartSeries[]
|
|
86
|
-
height?: number
|
|
87
|
-
curved?: boolean
|
|
88
|
-
showDots?: boolean
|
|
89
|
-
showGrid?: boolean
|
|
90
|
-
showLegend?: boolean
|
|
91
|
-
className?: string
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function LineChart({
|
|
95
|
-
data,
|
|
96
|
-
xKey,
|
|
97
|
-
series,
|
|
98
|
-
height = 280,
|
|
99
|
-
curved = true,
|
|
100
|
-
showDots = false,
|
|
101
|
-
showGrid = true,
|
|
102
|
-
showLegend = false,
|
|
103
|
-
className,
|
|
104
|
-
}: LineChartProps) {
|
|
105
|
-
return (
|
|
106
|
-
<div className={cx("w-full", className)} style={{ height }}>
|
|
107
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
108
|
-
<ReLineChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
109
|
-
{showGrid && <CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" vertical={false} />}
|
|
110
|
-
<XAxis dataKey={xKey} {...AXIS_STYLE} />
|
|
111
|
-
<YAxis {...AXIS_STYLE} />
|
|
112
|
-
<Tooltip content={<CustomTooltip />} />
|
|
113
|
-
{showLegend && <Legend wrapperStyle={{ fontSize: 11, color: "var(--color-muted-foreground)" }} />}
|
|
114
|
-
{series.map((s, i) => (
|
|
115
|
-
<Line
|
|
116
|
-
key={s.key}
|
|
117
|
-
type={curved ? "monotone" : "linear"}
|
|
118
|
-
dataKey={s.key}
|
|
119
|
-
name={s.label ?? s.key}
|
|
120
|
-
stroke={s.color ?? PALETTE[i % PALETTE.length]}
|
|
121
|
-
strokeWidth={2}
|
|
122
|
-
dot={showDots ? { r: 3, fill: s.color ?? PALETTE[i % PALETTE.length], strokeWidth: 0 } : false}
|
|
123
|
-
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
124
|
-
/>
|
|
125
|
-
))}
|
|
126
|
-
</ReLineChart>
|
|
127
|
-
</ResponsiveContainer>
|
|
128
|
-
</div>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ── BarChart ──────────────────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
export interface BarChartSeries {
|
|
135
|
-
key: string
|
|
136
|
-
label?: string
|
|
137
|
-
color?: string
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface BarChartProps {
|
|
141
|
-
data: Record<string, unknown>[]
|
|
142
|
-
xKey: string
|
|
143
|
-
series: BarChartSeries[]
|
|
144
|
-
height?: number
|
|
145
|
-
horizontal?: boolean
|
|
146
|
-
stacked?: boolean
|
|
147
|
-
showGrid?: boolean
|
|
148
|
-
showLegend?: boolean
|
|
149
|
-
radius?: number
|
|
150
|
-
className?: string
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function BarChart({
|
|
154
|
-
data,
|
|
155
|
-
xKey,
|
|
156
|
-
series,
|
|
157
|
-
height = 280,
|
|
158
|
-
horizontal = false,
|
|
159
|
-
stacked = false,
|
|
160
|
-
showGrid = true,
|
|
161
|
-
showLegend = false,
|
|
162
|
-
radius = 4,
|
|
163
|
-
className,
|
|
164
|
-
}: BarChartProps) {
|
|
165
|
-
return (
|
|
166
|
-
<div className={cx("w-full", className)} style={{ height }}>
|
|
167
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
168
|
-
<ReBarChart
|
|
169
|
-
data={data}
|
|
170
|
-
layout={horizontal ? "vertical" : "horizontal"}
|
|
171
|
-
margin={{ top: 4, right: 4, left: horizontal ? 8 : -20, bottom: 0 }}
|
|
172
|
-
>
|
|
173
|
-
{showGrid && (
|
|
174
|
-
<CartesianGrid
|
|
175
|
-
strokeDasharray="3 3"
|
|
176
|
-
stroke="var(--color-border)"
|
|
177
|
-
vertical={!horizontal}
|
|
178
|
-
horizontal={horizontal}
|
|
179
|
-
/>
|
|
180
|
-
)}
|
|
181
|
-
{horizontal ? (
|
|
182
|
-
<>
|
|
183
|
-
<XAxis type="number" {...AXIS_STYLE} />
|
|
184
|
-
<YAxis type="category" dataKey={xKey} {...AXIS_STYLE} width={60} />
|
|
185
|
-
</>
|
|
186
|
-
) : (
|
|
187
|
-
<>
|
|
188
|
-
<XAxis dataKey={xKey} {...AXIS_STYLE} />
|
|
189
|
-
<YAxis {...AXIS_STYLE} />
|
|
190
|
-
</>
|
|
191
|
-
)}
|
|
192
|
-
<Tooltip content={<CustomTooltip />} />
|
|
193
|
-
{showLegend && <Legend wrapperStyle={{ fontSize: 11, color: "var(--color-muted-foreground)" }} />}
|
|
194
|
-
{series.map((s, i) => (
|
|
195
|
-
<Bar
|
|
196
|
-
key={s.key}
|
|
197
|
-
dataKey={s.key}
|
|
198
|
-
name={s.label ?? s.key}
|
|
199
|
-
fill={s.color ?? PALETTE[i % PALETTE.length]}
|
|
200
|
-
stackId={stacked ? "stack" : undefined}
|
|
201
|
-
radius={i === series.length - 1 || !stacked ? radius : 0}
|
|
202
|
-
/>
|
|
203
|
-
))}
|
|
204
|
-
</ReBarChart>
|
|
205
|
-
</ResponsiveContainer>
|
|
206
|
-
</div>
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── AreaChart ─────────────────────────────────────────────────────────────────
|
|
211
|
-
|
|
212
|
-
export interface AreaChartSeries {
|
|
213
|
-
key: string
|
|
214
|
-
label?: string
|
|
215
|
-
color?: string
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export interface AreaChartProps {
|
|
219
|
-
data: Record<string, unknown>[]
|
|
220
|
-
xKey: string
|
|
221
|
-
series: AreaChartSeries[]
|
|
222
|
-
height?: number
|
|
223
|
-
curved?: boolean
|
|
224
|
-
stacked?: boolean
|
|
225
|
-
showGrid?: boolean
|
|
226
|
-
showLegend?: boolean
|
|
227
|
-
className?: string
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function AreaChart({
|
|
231
|
-
data,
|
|
232
|
-
xKey,
|
|
233
|
-
series,
|
|
234
|
-
height = 280,
|
|
235
|
-
curved = true,
|
|
236
|
-
stacked = false,
|
|
237
|
-
showGrid = true,
|
|
238
|
-
showLegend = false,
|
|
239
|
-
className,
|
|
240
|
-
}: AreaChartProps) {
|
|
241
|
-
return (
|
|
242
|
-
<div className={cx("w-full", className)} style={{ height }}>
|
|
243
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
244
|
-
<ReAreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
245
|
-
<defs>
|
|
246
|
-
{series.map((s, i) => {
|
|
247
|
-
const color = s.color ?? PALETTE[i % PALETTE.length]
|
|
248
|
-
return (
|
|
249
|
-
<linearGradient key={s.key} id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
|
|
250
|
-
<stop offset="5%" stopColor={color} stopOpacity={0.15} />
|
|
251
|
-
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
|
252
|
-
</linearGradient>
|
|
253
|
-
)
|
|
254
|
-
})}
|
|
255
|
-
</defs>
|
|
256
|
-
{showGrid && <CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" vertical={false} />}
|
|
257
|
-
<XAxis dataKey={xKey} {...AXIS_STYLE} />
|
|
258
|
-
<YAxis {...AXIS_STYLE} />
|
|
259
|
-
<Tooltip content={<CustomTooltip />} />
|
|
260
|
-
{showLegend && <Legend wrapperStyle={{ fontSize: 11, color: "var(--color-muted-foreground)" }} />}
|
|
261
|
-
{series.map((s, i) => {
|
|
262
|
-
const color = s.color ?? PALETTE[i % PALETTE.length]
|
|
263
|
-
return (
|
|
264
|
-
<Area
|
|
265
|
-
key={s.key}
|
|
266
|
-
type={curved ? "monotone" : "linear"}
|
|
267
|
-
dataKey={s.key}
|
|
268
|
-
name={s.label ?? s.key}
|
|
269
|
-
stroke={color}
|
|
270
|
-
strokeWidth={2}
|
|
271
|
-
fill={`url(#grad-${s.key})`}
|
|
272
|
-
stackId={stacked ? "stack" : undefined}
|
|
273
|
-
/>
|
|
274
|
-
)
|
|
275
|
-
})}
|
|
276
|
-
</ReAreaChart>
|
|
277
|
-
</ResponsiveContainer>
|
|
278
|
-
</div>
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// ── PieChart ──────────────────────────────────────────────────────────────────
|
|
283
|
-
|
|
284
|
-
export interface PieSlice {
|
|
285
|
-
name: string
|
|
286
|
-
value: number
|
|
287
|
-
color?: string
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export interface PieChartProps {
|
|
291
|
-
data: PieSlice[]
|
|
292
|
-
height?: number
|
|
293
|
-
donut?: boolean
|
|
294
|
-
showLabels?: boolean
|
|
295
|
-
showLegend?: boolean
|
|
296
|
-
className?: string
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
interface PieLabelProps {
|
|
300
|
-
cx?: number
|
|
301
|
-
cy?: number
|
|
302
|
-
midAngle?: number
|
|
303
|
-
innerRadius?: number
|
|
304
|
-
outerRadius?: number
|
|
305
|
-
name?: string
|
|
306
|
-
percent?: number
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function PieLabel({ cx, cy, midAngle, innerRadius, outerRadius, name, percent }: PieLabelProps) {
|
|
310
|
-
if (
|
|
311
|
-
cx == null ||
|
|
312
|
-
cy == null ||
|
|
313
|
-
midAngle == null ||
|
|
314
|
-
innerRadius == null ||
|
|
315
|
-
outerRadius == null ||
|
|
316
|
-
percent == null ||
|
|
317
|
-
percent < 0.05
|
|
318
|
-
) {
|
|
319
|
-
return null
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const RADIAN = Math.PI / 180
|
|
323
|
-
const radius = innerRadius + (outerRadius - innerRadius) * 0.5
|
|
324
|
-
const x = cx + radius * Math.cos(-midAngle * RADIAN)
|
|
325
|
-
const y = cy + radius * Math.sin(-midAngle * RADIAN)
|
|
326
|
-
return (
|
|
327
|
-
<text x={x} y={y} fill="var(--color-background)" textAnchor="middle" dominantBaseline="central" fontSize={11} fontWeight={500}>
|
|
328
|
-
{name}
|
|
329
|
-
</text>
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function PieChart({
|
|
334
|
-
data,
|
|
335
|
-
height = 280,
|
|
336
|
-
donut = false,
|
|
337
|
-
showLabels = false,
|
|
338
|
-
showLegend = true,
|
|
339
|
-
className,
|
|
340
|
-
}: PieChartProps) {
|
|
341
|
-
const outerRadius = 100
|
|
342
|
-
const innerRadius = donut ? 60 : 0
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
<div className={cx("w-full", className)} style={{ height }}>
|
|
346
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
347
|
-
<RePieChart>
|
|
348
|
-
<Pie
|
|
349
|
-
data={data}
|
|
350
|
-
dataKey="value"
|
|
351
|
-
nameKey="name"
|
|
352
|
-
cx="50%"
|
|
353
|
-
cy="50%"
|
|
354
|
-
outerRadius={outerRadius}
|
|
355
|
-
innerRadius={innerRadius}
|
|
356
|
-
strokeWidth={2}
|
|
357
|
-
stroke="var(--color-card)"
|
|
358
|
-
labelLine={false}
|
|
359
|
-
label={showLabels ? PieLabel : undefined}
|
|
360
|
-
>
|
|
361
|
-
{data.map((entry, i) => (
|
|
362
|
-
<Cell key={i} fill={entry.color ?? PALETTE[i % PALETTE.length]} />
|
|
363
|
-
))}
|
|
364
|
-
</Pie>
|
|
365
|
-
<Tooltip content={<CustomTooltip />} />
|
|
366
|
-
{showLegend && (
|
|
367
|
-
<Legend
|
|
368
|
-
wrapperStyle={{ fontSize: 11, color: "var(--color-muted-foreground)" }}
|
|
369
|
-
iconType="circle"
|
|
370
|
-
iconSize={8}
|
|
371
|
-
/>
|
|
372
|
-
)}
|
|
373
|
-
</RePieChart>
|
|
374
|
-
</ResponsiveContainer>
|
|
375
|
-
</div>
|
|
376
|
-
)
|
|
377
|
-
}
|