opentradex 0.1.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/.env.example +8 -0
- package/CLAUDE.md +98 -0
- package/README.md +246 -0
- package/SOUL.md +79 -0
- package/SPEC.md +317 -0
- package/SUBMISSION.md +30 -0
- package/architecture.excalidraw +170 -0
- package/architecture.png +0 -0
- package/bin/opentradex.mjs +4 -0
- package/data/.gitkeep +0 -0
- package/data/strategy_notes.md +158 -0
- package/gossip/__init__.py +0 -0
- package/gossip/dashboard.py +150 -0
- package/gossip/db.py +358 -0
- package/gossip/kalshi.py +492 -0
- package/gossip/news.py +235 -0
- package/gossip/trader.py +646 -0
- package/main.py +287 -0
- package/package.json +47 -0
- package/requirements.txt +7 -0
- package/src/cli.mjs +124 -0
- package/src/index.mjs +420 -0
- package/web/AGENTS.md +5 -0
- package/web/CLAUDE.md +1 -0
- package/web/README.md +36 -0
- package/web/components.json +25 -0
- package/web/eslint.config.mjs +18 -0
- package/web/next.config.ts +7 -0
- package/web/package-lock.json +11626 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +7 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/src/app/api/agent/route.ts +77 -0
- package/web/src/app/api/agent/stream/route.ts +87 -0
- package/web/src/app/api/markets/route.ts +15 -0
- package/web/src/app/api/news/live/route.ts +77 -0
- package/web/src/app/api/news/reddit/route.ts +118 -0
- package/web/src/app/api/news/route.ts +10 -0
- package/web/src/app/api/news/tiktok/route.ts +115 -0
- package/web/src/app/api/news/truthsocial/route.ts +116 -0
- package/web/src/app/api/news/twitter/route.ts +186 -0
- package/web/src/app/api/portfolio/route.ts +50 -0
- package/web/src/app/api/prices/route.ts +18 -0
- package/web/src/app/api/trades/route.ts +10 -0
- package/web/src/app/favicon.ico +0 -0
- package/web/src/app/globals.css +170 -0
- package/web/src/app/layout.tsx +36 -0
- package/web/src/app/page.tsx +366 -0
- package/web/src/components/AgentLog.tsx +71 -0
- package/web/src/components/LiveStream.tsx +394 -0
- package/web/src/components/MarketScanner.tsx +111 -0
- package/web/src/components/NewsFeed.tsx +561 -0
- package/web/src/components/PortfolioStrip.tsx +139 -0
- package/web/src/components/PositionsPanel.tsx +219 -0
- package/web/src/components/TopBar.tsx +127 -0
- package/web/src/components/ui/badge.tsx +52 -0
- package/web/src/components/ui/button.tsx +60 -0
- package/web/src/components/ui/card.tsx +103 -0
- package/web/src/components/ui/scroll-area.tsx +55 -0
- package/web/src/components/ui/separator.tsx +25 -0
- package/web/src/components/ui/tabs.tsx +82 -0
- package/web/src/components/ui/tooltip.tsx +66 -0
- package/web/src/lib/db.ts +81 -0
- package/web/src/lib/types.ts +130 -0
- package/web/src/lib/utils.ts +6 -0
- package/web/tsconfig.json +34 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
5
|
+
import { ExternalLink, ChevronDown, ChevronRight } from "lucide-react";
|
|
6
|
+
import type { Trade, PositionPrice } from "@/lib/types";
|
|
7
|
+
import { kalshiUrl } from "@/lib/types";
|
|
8
|
+
|
|
9
|
+
interface PositionsPanelProps {
|
|
10
|
+
positions: Trade[];
|
|
11
|
+
trades: Trade[];
|
|
12
|
+
prices?: PositionPrice[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PositionsPanel({ positions, trades, prices = [] }: PositionsPanelProps) {
|
|
16
|
+
const priceMap = new Map(prices.map((p) => [p.ticker, p]));
|
|
17
|
+
const totalUnrealized = prices.reduce((sum, p) => sum + p.unrealized_pnl, 0);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
21
|
+
<div className="h-9 flex items-center justify-between px-3 border-b border-border bg-card shrink-0">
|
|
22
|
+
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
23
|
+
Positions
|
|
24
|
+
</span>
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
{prices.length > 0 && (
|
|
27
|
+
<span
|
|
28
|
+
className={`text-[10px] font-mono font-medium ${
|
|
29
|
+
totalUnrealized >= 0 ? "text-primary" : "text-destructive"
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
{totalUnrealized >= 0 ? "+" : ""}${totalUnrealized.toFixed(2)}
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
<span className="text-[10px] text-muted-foreground/50 font-mono">
|
|
36
|
+
{positions.length}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
41
|
+
<div className="p-2 space-y-1">
|
|
42
|
+
{positions.length === 0 && (
|
|
43
|
+
<p className="text-xs text-muted-foreground/40 py-8 text-center">
|
|
44
|
+
No open positions
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
{positions.map((t) => (
|
|
48
|
+
<PositionCard key={t.id} trade={t} price={priceMap.get(t.ticker)} />
|
|
49
|
+
))}
|
|
50
|
+
|
|
51
|
+
{trades.length > 0 && (
|
|
52
|
+
<div className="pt-2">
|
|
53
|
+
<p className="text-[10px] text-muted-foreground/50 uppercase tracking-wider font-medium px-2 pb-1">
|
|
54
|
+
Recent Trades
|
|
55
|
+
</p>
|
|
56
|
+
{trades.slice(0, 10).map((t) => (
|
|
57
|
+
<TradeRow key={t.id} trade={t} />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</ScrollArea>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function PositionCard({ trade: t, price }: { trade: Trade; price?: PositionPrice }) {
|
|
68
|
+
const [expanded, setExpanded] = useState(false);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="rounded-md bg-secondary/20 overflow-hidden">
|
|
72
|
+
<button
|
|
73
|
+
className="w-full text-left px-3 py-2.5 hover:bg-secondary/30 transition-colors"
|
|
74
|
+
onClick={() => setExpanded(!expanded)}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center justify-between gap-2">
|
|
77
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
78
|
+
<span className="text-muted-foreground/40 shrink-0">
|
|
79
|
+
{expanded ? (
|
|
80
|
+
<ChevronDown className="h-3 w-3" />
|
|
81
|
+
) : (
|
|
82
|
+
<ChevronRight className="h-3 w-3" />
|
|
83
|
+
)}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="font-mono text-[11px] font-medium truncate">
|
|
86
|
+
{t.ticker}
|
|
87
|
+
</span>
|
|
88
|
+
<a
|
|
89
|
+
href={kalshiUrl(t.ticker, t.title)}
|
|
90
|
+
target="_blank"
|
|
91
|
+
rel="noopener noreferrer"
|
|
92
|
+
className="shrink-0 text-muted-foreground/30 hover:text-primary transition-colors"
|
|
93
|
+
onClick={(e) => e.stopPropagation()}
|
|
94
|
+
>
|
|
95
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
99
|
+
{price && (
|
|
100
|
+
<span
|
|
101
|
+
className={`text-[10px] font-mono font-medium ${
|
|
102
|
+
price.unrealized_pnl >= 0 ? "text-primary" : "text-destructive"
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
{price.unrealized_pnl >= 0 ? "+" : ""}${price.unrealized_pnl.toFixed(2)}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
<span
|
|
109
|
+
className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
|
|
110
|
+
t.side === "yes"
|
|
111
|
+
? "bg-primary/15 text-primary"
|
|
112
|
+
: "bg-destructive/15 text-destructive"
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
{t.side.toUpperCase()}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<p className="text-[10px] text-muted-foreground/50 truncate mt-1 pl-[18px]">
|
|
121
|
+
{t.title}
|
|
122
|
+
</p>
|
|
123
|
+
|
|
124
|
+
<div className="flex items-center gap-3 mt-1 pl-[18px] text-[10px]">
|
|
125
|
+
<span className="text-muted-foreground/60 font-mono">
|
|
126
|
+
{t.contracts}x @ ${t.entry_price.toFixed(2)}
|
|
127
|
+
</span>
|
|
128
|
+
{price && (
|
|
129
|
+
<span className="text-muted-foreground/40 font-mono">
|
|
130
|
+
now ${price.mark_price.toFixed(2)}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
<span className="text-primary font-medium">
|
|
134
|
+
{(t.edge * 100).toFixed(1)}pp
|
|
135
|
+
</span>
|
|
136
|
+
<span className="text-muted-foreground/40 capitalize text-[9px]">
|
|
137
|
+
{t.confidence}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
</button>
|
|
141
|
+
|
|
142
|
+
{expanded && t.reasoning && (
|
|
143
|
+
<div className="px-3 pb-2.5 border-t border-border/20">
|
|
144
|
+
<div className="pl-[18px] pt-2">
|
|
145
|
+
<p className="text-[11px] text-foreground/70 leading-relaxed whitespace-pre-wrap">
|
|
146
|
+
{t.reasoning}
|
|
147
|
+
</p>
|
|
148
|
+
{t.estimated_prob > 0 && (
|
|
149
|
+
<div className="flex gap-4 mt-2 text-[10px] text-muted-foreground/50 font-mono">
|
|
150
|
+
<span>est {(t.estimated_prob * 100).toFixed(0)}%</span>
|
|
151
|
+
<span>mkt {(t.entry_price * 100).toFixed(0)}%</span>
|
|
152
|
+
<span className="text-primary/70">
|
|
153
|
+
edge {(t.edge * 100).toFixed(1)}pp
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function TradeRow({ trade: t }: { trade: Trade }) {
|
|
165
|
+
return (
|
|
166
|
+
<a
|
|
167
|
+
href={kalshiUrl(t.ticker, t.title)}
|
|
168
|
+
target="_blank"
|
|
169
|
+
rel="noopener noreferrer"
|
|
170
|
+
className="flex items-center justify-between px-2 py-1.5 rounded hover:bg-secondary/20 transition-colors group"
|
|
171
|
+
>
|
|
172
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
173
|
+
<span
|
|
174
|
+
className={`text-[10px] font-mono ${
|
|
175
|
+
t.outcome === "win"
|
|
176
|
+
? "text-primary"
|
|
177
|
+
: t.outcome === "loss"
|
|
178
|
+
? "text-destructive"
|
|
179
|
+
: "text-muted-foreground/40"
|
|
180
|
+
}`}
|
|
181
|
+
>
|
|
182
|
+
{t.outcome === "win" ? "+" : t.outcome === "loss" ? "-" : "~"}
|
|
183
|
+
</span>
|
|
184
|
+
<span className="font-mono text-[10px] truncate text-muted-foreground/70 group-hover:text-foreground/80 transition-colors">
|
|
185
|
+
{t.ticker}
|
|
186
|
+
</span>
|
|
187
|
+
<span
|
|
188
|
+
className={`text-[9px] ${t.side === "yes" ? "text-primary/50" : "text-destructive/50"}`}
|
|
189
|
+
>
|
|
190
|
+
{t.side.toUpperCase()}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="flex items-center gap-2 text-[10px] shrink-0">
|
|
194
|
+
{t.settled ? (
|
|
195
|
+
<span
|
|
196
|
+
className={`font-mono ${t.pnl >= 0 ? "text-primary/70" : "text-destructive/70"}`}
|
|
197
|
+
>
|
|
198
|
+
{t.pnl >= 0 ? "+" : ""}${t.pnl.toFixed(2)}
|
|
199
|
+
</span>
|
|
200
|
+
) : (
|
|
201
|
+
<span className="text-muted-foreground/30">open</span>
|
|
202
|
+
)}
|
|
203
|
+
<span className="text-muted-foreground/25 text-[9px]">
|
|
204
|
+
{formatRelativeTime(t.timestamp)}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
</a>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatRelativeTime(ts: string): string {
|
|
212
|
+
const diff = Date.now() - new Date(ts).getTime();
|
|
213
|
+
const mins = Math.floor(diff / 60000);
|
|
214
|
+
if (mins < 1) return "now";
|
|
215
|
+
if (mins < 60) return `${mins}m`;
|
|
216
|
+
const hrs = Math.floor(mins / 60);
|
|
217
|
+
if (hrs < 24) return `${hrs}h`;
|
|
218
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
219
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Activity, Play, Repeat, RotateCw } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
interface TopBarProps {
|
|
7
|
+
liveStatus: string;
|
|
8
|
+
agentStatus: string;
|
|
9
|
+
rationale: string;
|
|
10
|
+
loopInterval: number;
|
|
11
|
+
onRationaleChange: (v: string) => void;
|
|
12
|
+
onSubmitRationale: () => void;
|
|
13
|
+
onRunCycle: () => void;
|
|
14
|
+
onStartLoop: () => void;
|
|
15
|
+
onRefresh: () => void;
|
|
16
|
+
onLoopIntervalChange: (v: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const INTERVALS = [
|
|
20
|
+
{ value: 60, label: "1m" },
|
|
21
|
+
{ value: 300, label: "5m" },
|
|
22
|
+
{ value: 600, label: "10m" },
|
|
23
|
+
{ value: 900, label: "15m" },
|
|
24
|
+
{ value: 1800, label: "30m" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function TopBar({
|
|
28
|
+
liveStatus,
|
|
29
|
+
agentStatus,
|
|
30
|
+
rationale,
|
|
31
|
+
loopInterval,
|
|
32
|
+
onRationaleChange,
|
|
33
|
+
onSubmitRationale,
|
|
34
|
+
onRunCycle,
|
|
35
|
+
onStartLoop,
|
|
36
|
+
onRefresh,
|
|
37
|
+
onLoopIntervalChange,
|
|
38
|
+
}: TopBarProps) {
|
|
39
|
+
return (
|
|
40
|
+
<header className="h-14 flex items-center gap-4 px-5 border-b border-border bg-card shrink-0 overflow-hidden max-w-full">
|
|
41
|
+
<div className="flex items-center gap-3 mr-2">
|
|
42
|
+
<Activity className="h-5 w-5 text-primary" />
|
|
43
|
+
<span className="font-semibold text-sm tracking-tight">
|
|
44
|
+
Open Trademaxxxing
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="flex items-center gap-2">
|
|
49
|
+
<span
|
|
50
|
+
className={`w-2 h-2 rounded-full shrink-0 ${
|
|
51
|
+
liveStatus === "running"
|
|
52
|
+
? "bg-primary pulse-glow"
|
|
53
|
+
: liveStatus === "error"
|
|
54
|
+
? "bg-destructive"
|
|
55
|
+
: "bg-muted-foreground/40"
|
|
56
|
+
}`}
|
|
57
|
+
/>
|
|
58
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
|
59
|
+
{liveStatus === "running"
|
|
60
|
+
? "Running"
|
|
61
|
+
: liveStatus === "error"
|
|
62
|
+
? "Error"
|
|
63
|
+
: "Idle"}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{agentStatus && (
|
|
68
|
+
<span className="text-xs text-muted-foreground/60 truncate max-w-48">
|
|
69
|
+
{agentStatus}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div className="flex-1 max-w-lg ml-auto">
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<input
|
|
76
|
+
type="text"
|
|
77
|
+
value={rationale}
|
|
78
|
+
onChange={(e) => onRationaleChange(e.target.value)}
|
|
79
|
+
onKeyDown={(e) => e.key === "Enter" && onSubmitRationale()}
|
|
80
|
+
placeholder="Enter a thesis to research & trade..."
|
|
81
|
+
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 placeholder:text-muted-foreground/40 transition-all"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
87
|
+
{INTERVALS.map((i) => (
|
|
88
|
+
<button
|
|
89
|
+
key={i.value}
|
|
90
|
+
onClick={() => onLoopIntervalChange(i.value)}
|
|
91
|
+
className={`px-2 py-1 rounded text-[10px] font-medium transition-colors ${
|
|
92
|
+
loopInterval === i.value
|
|
93
|
+
? "bg-primary text-primary-foreground"
|
|
94
|
+
: "text-muted-foreground hover:text-foreground hover:bg-secondary"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{i.label}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
103
|
+
<Button size="sm" onClick={onRunCycle} className="h-7 text-xs gap-1.5">
|
|
104
|
+
<Play className="h-3 w-3" />
|
|
105
|
+
Run
|
|
106
|
+
</Button>
|
|
107
|
+
<Button
|
|
108
|
+
size="sm"
|
|
109
|
+
variant="secondary"
|
|
110
|
+
onClick={onStartLoop}
|
|
111
|
+
className="h-7 text-xs gap-1.5"
|
|
112
|
+
>
|
|
113
|
+
<Repeat className="h-3 w-3" />
|
|
114
|
+
Loop
|
|
115
|
+
</Button>
|
|
116
|
+
<Button
|
|
117
|
+
size="icon"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
onClick={onRefresh}
|
|
120
|
+
className="h-7 w-7"
|
|
121
|
+
>
|
|
122
|
+
<RotateCw className="h-3.5 w-3.5" />
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</header>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -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,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"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",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
14
|
+
outline:
|
|
15
|
+
"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",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
|
+
ghost:
|
|
19
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
20
|
+
destructive:
|
|
21
|
+
"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",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default:
|
|
26
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
27
|
+
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",
|
|
28
|
+
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",
|
|
29
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
30
|
+
icon: "size-8",
|
|
31
|
+
"icon-xs":
|
|
32
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
33
|
+
"icon-sm":
|
|
34
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
35
|
+
"icon-lg": "size-9",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
variant: "default",
|
|
40
|
+
size: "default",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
function Button({
|
|
46
|
+
className,
|
|
47
|
+
variant = "default",
|
|
48
|
+
size = "default",
|
|
49
|
+
...props
|
|
50
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
51
|
+
return (
|
|
52
|
+
<ButtonPrimitive
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
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,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function ScrollArea({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: ScrollAreaPrimitive.Root.Props) {
|
|
13
|
+
return (
|
|
14
|
+
<ScrollAreaPrimitive.Root
|
|
15
|
+
data-slot="scroll-area"
|
|
16
|
+
className={cn("relative", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<ScrollAreaPrimitive.Viewport
|
|
20
|
+
data-slot="scroll-area-viewport"
|
|
21
|
+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</ScrollAreaPrimitive.Viewport>
|
|
25
|
+
<ScrollBar />
|
|
26
|
+
<ScrollAreaPrimitive.Corner />
|
|
27
|
+
</ScrollAreaPrimitive.Root>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ScrollBar({
|
|
32
|
+
className,
|
|
33
|
+
orientation = "vertical",
|
|
34
|
+
...props
|
|
35
|
+
}: ScrollAreaPrimitive.Scrollbar.Props) {
|
|
36
|
+
return (
|
|
37
|
+
<ScrollAreaPrimitive.Scrollbar
|
|
38
|
+
data-slot="scroll-area-scrollbar"
|
|
39
|
+
data-orientation={orientation}
|
|
40
|
+
orientation={orientation}
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<ScrollAreaPrimitive.Thumb
|
|
48
|
+
data-slot="scroll-area-thumb"
|
|
49
|
+
className="relative flex-1 rounded-full bg-border"
|
|
50
|
+
/>
|
|
51
|
+
</ScrollAreaPrimitive.Scrollbar>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { ScrollArea, ScrollBar }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Separator({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
10
|
+
...props
|
|
11
|
+
}: SeparatorPrimitive.Props) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
orientation={orientation}
|
|
16
|
+
className={cn(
|
|
17
|
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { Separator }
|