context-mode 1.0.80 → 1.0.82
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/cli.js +57 -0
- package/build/server.js +94 -1
- package/cli.bundle.mjs +106 -99
- package/insight/components.json +25 -0
- package/insight/index.html +13 -0
- package/insight/package.json +54 -0
- package/insight/server.mjs +624 -0
- package/insight/src/components/analytics.tsx +112 -0
- package/insight/src/components/ui/badge.tsx +52 -0
- package/insight/src/components/ui/button.tsx +58 -0
- package/insight/src/components/ui/card.tsx +103 -0
- package/insight/src/components/ui/chart.tsx +371 -0
- package/insight/src/components/ui/collapsible.tsx +19 -0
- package/insight/src/components/ui/input.tsx +20 -0
- package/insight/src/components/ui/progress.tsx +83 -0
- package/insight/src/components/ui/scroll-area.tsx +55 -0
- package/insight/src/components/ui/separator.tsx +23 -0
- package/insight/src/components/ui/table.tsx +114 -0
- package/insight/src/components/ui/tabs.tsx +82 -0
- package/insight/src/components/ui/tooltip.tsx +64 -0
- package/insight/src/lib/api.ts +71 -0
- package/insight/src/lib/utils.ts +6 -0
- package/insight/src/main.tsx +22 -0
- package/insight/src/routeTree.gen.ts +189 -0
- package/insight/src/router.tsx +19 -0
- package/insight/src/routes/__root.tsx +55 -0
- package/insight/src/routes/enterprise.tsx +316 -0
- package/insight/src/routes/index.tsx +914 -0
- package/insight/src/routes/knowledge.tsx +221 -0
- package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +137 -0
- package/insight/src/routes/search.tsx +97 -0
- package/insight/src/routes/sessions.tsx +179 -0
- package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +181 -0
- package/insight/src/styles.css +104 -0
- package/insight/tsconfig.json +29 -0
- package/insight/vite.config.ts +19 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +76 -72
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
const COLORS = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#06b6d4", "#ec4899", "#84cc16"];
|
|
5
|
+
export { COLORS };
|
|
6
|
+
|
|
7
|
+
// ── Big number stat card ──
|
|
8
|
+
export function Stat({ label, value, sub, icon: Icon, color }: {
|
|
9
|
+
label: string; value: string | number; sub: string; icon: LucideIcon; color: string;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<Card>
|
|
13
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
14
|
+
<CardTitle className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">{label}</CardTitle>
|
|
15
|
+
<Icon className={`h-4 w-4 ${color}`} />
|
|
16
|
+
</CardHeader>
|
|
17
|
+
<CardContent>
|
|
18
|
+
<div className="text-2xl font-bold tabular-nums">{value}</div>
|
|
19
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">{sub}</p>
|
|
20
|
+
</CardContent>
|
|
21
|
+
</Card>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Mini number ──
|
|
26
|
+
export function Mini({ label, value, color }: { label: string; value: string | number; color?: string }) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="text-center">
|
|
29
|
+
<div className={`text-2xl font-bold tabular-nums ${color || ""}`}>{value}</div>
|
|
30
|
+
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">{label}</p>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Ratio bar ──
|
|
36
|
+
export function RatioBar({ items }: { items: { label: string; value: number; color: string }[] }) {
|
|
37
|
+
const total = items.reduce((a, b) => a + b.value, 0);
|
|
38
|
+
if (total === 0) return null;
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
<div className="flex h-3 rounded-full overflow-hidden bg-secondary">
|
|
42
|
+
{items.map((item, i) => (
|
|
43
|
+
<div key={i} className="transition-all" style={{
|
|
44
|
+
width: `${Math.max(Math.round(100 * item.value / total), 2)}%`,
|
|
45
|
+
background: item.color,
|
|
46
|
+
}} />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2">
|
|
50
|
+
{items.map((item, i) => (
|
|
51
|
+
<span key={i} className="text-[10px] text-muted-foreground flex items-center gap-1">
|
|
52
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: item.color }} />
|
|
53
|
+
{item.label}: {item.value} ({Math.round(100 * item.value / total)}%)
|
|
54
|
+
</span>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Relative time helper ──
|
|
62
|
+
export function timeAgo(dateStr: string | null | undefined): string {
|
|
63
|
+
if (!dateStr) return "-";
|
|
64
|
+
const d = new Date(dateStr);
|
|
65
|
+
if (isNaN(d.getTime())) return dateStr;
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const diffMs = now - d.getTime();
|
|
68
|
+
if (diffMs < 0) return "just now";
|
|
69
|
+
const mins = Math.floor(diffMs / 60000);
|
|
70
|
+
if (mins < 1) return "just now";
|
|
71
|
+
if (mins < 60) return `${mins}m ago`;
|
|
72
|
+
const hours = Math.floor(mins / 60);
|
|
73
|
+
if (hours < 24) return `${hours}h ago`;
|
|
74
|
+
const days = Math.floor(hours / 24);
|
|
75
|
+
if (days < 7) return `${days}d ago`;
|
|
76
|
+
const weeks = Math.floor(days / 7);
|
|
77
|
+
if (weeks < 5) return `${weeks}w ago`;
|
|
78
|
+
return `${Math.floor(days / 30)}mo ago`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Date group label ──
|
|
82
|
+
export function dateGroup(dateStr: string | null | undefined): string {
|
|
83
|
+
if (!dateStr) return "Older";
|
|
84
|
+
const d = new Date(dateStr);
|
|
85
|
+
if (isNaN(d.getTime())) return "Older";
|
|
86
|
+
const now = new Date();
|
|
87
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
88
|
+
const yesterday = new Date(today.getTime() - 86400000);
|
|
89
|
+
const weekAgo = new Date(today.getTime() - 7 * 86400000);
|
|
90
|
+
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
91
|
+
if (target >= today) return "Today";
|
|
92
|
+
if (target >= yesterday) return "Yesterday";
|
|
93
|
+
if (target >= weekAgo) return "This Week";
|
|
94
|
+
return "Older";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Parse source label into badges ──
|
|
98
|
+
export function parseSourceLabel(label: string): string[] {
|
|
99
|
+
let cleaned = label;
|
|
100
|
+
if (cleaned.startsWith("batch:")) cleaned = cleaned.slice(6);
|
|
101
|
+
const parts = cleaned.split(",").map(s => s.trim()).filter(Boolean);
|
|
102
|
+
return parts.slice(0, 3);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Duration formatter ──
|
|
106
|
+
export function formatDuration(mins: number): string {
|
|
107
|
+
if (mins < 1) return "<1m";
|
|
108
|
+
if (mins < 60) return `${Math.round(mins)}m`;
|
|
109
|
+
const h = Math.floor(mins / 60);
|
|
110
|
+
const m = Math.round(mins % 60);
|
|
111
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "#/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
17
|
+
outline:
|
|
18
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant = "default",
|
|
33
|
+
render,
|
|
34
|
+
...props
|
|
35
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
36
|
+
return useRender({
|
|
37
|
+
defaultTagName: "span",
|
|
38
|
+
props: mergeProps<"span">(
|
|
39
|
+
{
|
|
40
|
+
className: cn(badgeVariants({ variant }), className),
|
|
41
|
+
},
|
|
42
|
+
props
|
|
43
|
+
),
|
|
44
|
+
render,
|
|
45
|
+
state: {
|
|
46
|
+
slot: "badge",
|
|
47
|
+
variant,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "#/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
12
|
+
outline:
|
|
13
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
14
|
+
secondary:
|
|
15
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
16
|
+
ghost:
|
|
17
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
18
|
+
destructive:
|
|
19
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default:
|
|
24
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
25
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
28
|
+
icon: "size-8",
|
|
29
|
+
"icon-xs":
|
|
30
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
"icon-sm":
|
|
32
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
33
|
+
"icon-lg": "size-9",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "default",
|
|
38
|
+
size: "default",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function Button({
|
|
44
|
+
className,
|
|
45
|
+
variant = "default",
|
|
46
|
+
size = "default",
|
|
47
|
+
...props
|
|
48
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
49
|
+
return (
|
|
50
|
+
<ButtonPrimitive
|
|
51
|
+
data-slot="button"
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "#/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
14
|
+
className={cn(
|
|
15
|
+
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
className={cn(
|
|
41
|
+
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="card-description"
|
|
53
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
data-slot="card-action"
|
|
63
|
+
className={cn(
|
|
64
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="card-content"
|
|
76
|
+
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
data-slot="card-footer"
|
|
86
|
+
className={cn(
|
|
87
|
+
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
Card,
|
|
97
|
+
CardHeader,
|
|
98
|
+
CardFooter,
|
|
99
|
+
CardTitle,
|
|
100
|
+
CardAction,
|
|
101
|
+
CardDescription,
|
|
102
|
+
CardContent,
|
|
103
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as RechartsPrimitive from "recharts"
|
|
3
|
+
import type { TooltipValueType } from "recharts"
|
|
4
|
+
|
|
5
|
+
import { cn } from "#/lib/utils"
|
|
6
|
+
|
|
7
|
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
8
|
+
const THEMES = { light: "", dark: ".dark" } as const
|
|
9
|
+
|
|
10
|
+
const INITIAL_DIMENSION = { width: 320, height: 200 } as const
|
|
11
|
+
type TooltipNameType = number | string
|
|
12
|
+
|
|
13
|
+
export type ChartConfig = Record<
|
|
14
|
+
string,
|
|
15
|
+
{
|
|
16
|
+
label?: React.ReactNode
|
|
17
|
+
icon?: React.ComponentType
|
|
18
|
+
} & (
|
|
19
|
+
| { color?: string; theme?: never }
|
|
20
|
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
21
|
+
)
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
type ChartContextProps = {
|
|
25
|
+
config: ChartConfig
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
29
|
+
|
|
30
|
+
function useChart() {
|
|
31
|
+
const context = React.useContext(ChartContext)
|
|
32
|
+
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error("useChart must be used within a <ChartContainer />")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ChartContainer({
|
|
41
|
+
id,
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
config,
|
|
45
|
+
initialDimension = INITIAL_DIMENSION,
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<"div"> & {
|
|
48
|
+
config: ChartConfig
|
|
49
|
+
children: React.ComponentProps<
|
|
50
|
+
typeof RechartsPrimitive.ResponsiveContainer
|
|
51
|
+
>["children"]
|
|
52
|
+
initialDimension?: {
|
|
53
|
+
width: number
|
|
54
|
+
height: number
|
|
55
|
+
}
|
|
56
|
+
}) {
|
|
57
|
+
const uniqueId = React.useId()
|
|
58
|
+
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ChartContext.Provider value={{ config }}>
|
|
62
|
+
<div
|
|
63
|
+
data-slot="chart"
|
|
64
|
+
data-chart={chartId}
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
<ChartStyle id={chartId} config={config} />
|
|
72
|
+
<RechartsPrimitive.ResponsiveContainer
|
|
73
|
+
initialDimension={initialDimension}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
77
|
+
</div>
|
|
78
|
+
</ChartContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
83
|
+
const colorConfig = Object.entries(config).filter(
|
|
84
|
+
([, config]) => config.theme ?? config.color
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (!colorConfig.length) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<style
|
|
93
|
+
dangerouslySetInnerHTML={{
|
|
94
|
+
__html: Object.entries(THEMES)
|
|
95
|
+
.map(
|
|
96
|
+
([theme, prefix]) => `
|
|
97
|
+
${prefix} [data-chart=${id}] {
|
|
98
|
+
${colorConfig
|
|
99
|
+
.map(([key, itemConfig]) => {
|
|
100
|
+
const color =
|
|
101
|
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
|
|
102
|
+
itemConfig.color
|
|
103
|
+
return color ? ` --color-${key}: ${color};` : null
|
|
104
|
+
})
|
|
105
|
+
.join("\n")}
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
)
|
|
109
|
+
.join("\n"),
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
116
|
+
|
|
117
|
+
function ChartTooltipContent({
|
|
118
|
+
active,
|
|
119
|
+
payload,
|
|
120
|
+
className,
|
|
121
|
+
indicator = "dot",
|
|
122
|
+
hideLabel = false,
|
|
123
|
+
hideIndicator = false,
|
|
124
|
+
label,
|
|
125
|
+
labelFormatter,
|
|
126
|
+
labelClassName,
|
|
127
|
+
formatter,
|
|
128
|
+
color,
|
|
129
|
+
nameKey,
|
|
130
|
+
labelKey,
|
|
131
|
+
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
132
|
+
React.ComponentProps<"div"> & {
|
|
133
|
+
hideLabel?: boolean
|
|
134
|
+
hideIndicator?: boolean
|
|
135
|
+
indicator?: "line" | "dot" | "dashed"
|
|
136
|
+
nameKey?: string
|
|
137
|
+
labelKey?: string
|
|
138
|
+
} & Omit<
|
|
139
|
+
RechartsPrimitive.DefaultTooltipContentProps<
|
|
140
|
+
TooltipValueType,
|
|
141
|
+
TooltipNameType
|
|
142
|
+
>,
|
|
143
|
+
"accessibilityLayer"
|
|
144
|
+
>) {
|
|
145
|
+
const { config } = useChart()
|
|
146
|
+
|
|
147
|
+
const tooltipLabel = React.useMemo(() => {
|
|
148
|
+
if (hideLabel || !payload?.length) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const [item] = payload
|
|
153
|
+
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`
|
|
154
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
155
|
+
const value =
|
|
156
|
+
!labelKey && typeof label === "string"
|
|
157
|
+
? (config[label]?.label ?? label)
|
|
158
|
+
: itemConfig?.label
|
|
159
|
+
|
|
160
|
+
if (labelFormatter) {
|
|
161
|
+
return (
|
|
162
|
+
<div className={cn("font-medium", labelClassName)}>
|
|
163
|
+
{labelFormatter(value, payload)}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!value) {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
173
|
+
}, [
|
|
174
|
+
label,
|
|
175
|
+
labelFormatter,
|
|
176
|
+
payload,
|
|
177
|
+
hideLabel,
|
|
178
|
+
labelClassName,
|
|
179
|
+
config,
|
|
180
|
+
labelKey,
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
if (!active || !payload?.length) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
className={cn(
|
|
192
|
+
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
193
|
+
className
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
{!nestLabel ? tooltipLabel : null}
|
|
197
|
+
<div className="grid gap-1.5">
|
|
198
|
+
{payload
|
|
199
|
+
.filter((item) => item.type !== "none")
|
|
200
|
+
.map((item, index) => {
|
|
201
|
+
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
|
|
202
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
203
|
+
const indicatorColor = color ?? item.payload?.fill ?? item.color
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div
|
|
207
|
+
key={index}
|
|
208
|
+
className={cn(
|
|
209
|
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
210
|
+
indicator === "dot" && "items-center"
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{formatter && item?.value !== undefined && item.name ? (
|
|
214
|
+
formatter(item.value, item.name, item, index, item.payload)
|
|
215
|
+
) : (
|
|
216
|
+
<>
|
|
217
|
+
{itemConfig?.icon ? (
|
|
218
|
+
<itemConfig.icon />
|
|
219
|
+
) : (
|
|
220
|
+
!hideIndicator && (
|
|
221
|
+
<div
|
|
222
|
+
className={cn(
|
|
223
|
+
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
224
|
+
{
|
|
225
|
+
"h-2.5 w-2.5": indicator === "dot",
|
|
226
|
+
"w-1": indicator === "line",
|
|
227
|
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
228
|
+
indicator === "dashed",
|
|
229
|
+
"my-0.5": nestLabel && indicator === "dashed",
|
|
230
|
+
}
|
|
231
|
+
)}
|
|
232
|
+
style={
|
|
233
|
+
{
|
|
234
|
+
"--color-bg": indicatorColor,
|
|
235
|
+
"--color-border": indicatorColor,
|
|
236
|
+
} as React.CSSProperties
|
|
237
|
+
}
|
|
238
|
+
/>
|
|
239
|
+
)
|
|
240
|
+
)}
|
|
241
|
+
<div
|
|
242
|
+
className={cn(
|
|
243
|
+
"flex flex-1 justify-between leading-none",
|
|
244
|
+
nestLabel ? "items-end" : "items-center"
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<div className="grid gap-1.5">
|
|
248
|
+
{nestLabel ? tooltipLabel : null}
|
|
249
|
+
<span className="text-muted-foreground">
|
|
250
|
+
{itemConfig?.label ?? item.name}
|
|
251
|
+
</span>
|
|
252
|
+
</div>
|
|
253
|
+
{item.value != null && (
|
|
254
|
+
<span className="font-mono font-medium text-foreground tabular-nums">
|
|
255
|
+
{typeof item.value === "number"
|
|
256
|
+
? item.value.toLocaleString()
|
|
257
|
+
: String(item.value)}
|
|
258
|
+
</span>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const ChartLegend = RechartsPrimitive.Legend
|
|
272
|
+
|
|
273
|
+
function ChartLegendContent({
|
|
274
|
+
className,
|
|
275
|
+
hideIcon = false,
|
|
276
|
+
payload,
|
|
277
|
+
verticalAlign = "bottom",
|
|
278
|
+
nameKey,
|
|
279
|
+
}: React.ComponentProps<"div"> & {
|
|
280
|
+
hideIcon?: boolean
|
|
281
|
+
nameKey?: string
|
|
282
|
+
} & RechartsPrimitive.DefaultLegendContentProps) {
|
|
283
|
+
const { config } = useChart()
|
|
284
|
+
|
|
285
|
+
if (!payload?.length) {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
className={cn(
|
|
292
|
+
"flex items-center justify-center gap-4",
|
|
293
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
294
|
+
className
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
{payload
|
|
298
|
+
.filter((item) => item.type !== "none")
|
|
299
|
+
.map((item, index) => {
|
|
300
|
+
const key = `${nameKey ?? item.dataKey ?? "value"}`
|
|
301
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div
|
|
305
|
+
key={index}
|
|
306
|
+
className={cn(
|
|
307
|
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
{itemConfig?.icon && !hideIcon ? (
|
|
311
|
+
<itemConfig.icon />
|
|
312
|
+
) : (
|
|
313
|
+
<div
|
|
314
|
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
315
|
+
style={{
|
|
316
|
+
backgroundColor: item.color,
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
{itemConfig?.label}
|
|
321
|
+
</div>
|
|
322
|
+
)
|
|
323
|
+
})}
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getPayloadConfigFromPayload(
|
|
329
|
+
config: ChartConfig,
|
|
330
|
+
payload: unknown,
|
|
331
|
+
key: string
|
|
332
|
+
) {
|
|
333
|
+
if (typeof payload !== "object" || payload === null) {
|
|
334
|
+
return undefined
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const payloadPayload =
|
|
338
|
+
"payload" in payload &&
|
|
339
|
+
typeof payload.payload === "object" &&
|
|
340
|
+
payload.payload !== null
|
|
341
|
+
? payload.payload
|
|
342
|
+
: undefined
|
|
343
|
+
|
|
344
|
+
let configLabelKey: string = key
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
key in payload &&
|
|
348
|
+
typeof payload[key as keyof typeof payload] === "string"
|
|
349
|
+
) {
|
|
350
|
+
configLabelKey = payload[key as keyof typeof payload] as string
|
|
351
|
+
} else if (
|
|
352
|
+
payloadPayload &&
|
|
353
|
+
key in payloadPayload &&
|
|
354
|
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
355
|
+
) {
|
|
356
|
+
configLabelKey = payloadPayload[
|
|
357
|
+
key as keyof typeof payloadPayload
|
|
358
|
+
] as string
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return configLabelKey in config ? config[configLabelKey] : config[key]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export {
|
|
365
|
+
ChartContainer,
|
|
366
|
+
ChartTooltip,
|
|
367
|
+
ChartTooltipContent,
|
|
368
|
+
ChartLegend,
|
|
369
|
+
ChartLegendContent,
|
|
370
|
+
ChartStyle,
|
|
371
|
+
}
|