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.
Files changed (70) hide show
  1. package/.env.example +8 -0
  2. package/CLAUDE.md +98 -0
  3. package/README.md +246 -0
  4. package/SOUL.md +79 -0
  5. package/SPEC.md +317 -0
  6. package/SUBMISSION.md +30 -0
  7. package/architecture.excalidraw +170 -0
  8. package/architecture.png +0 -0
  9. package/bin/opentradex.mjs +4 -0
  10. package/data/.gitkeep +0 -0
  11. package/data/strategy_notes.md +158 -0
  12. package/gossip/__init__.py +0 -0
  13. package/gossip/dashboard.py +150 -0
  14. package/gossip/db.py +358 -0
  15. package/gossip/kalshi.py +492 -0
  16. package/gossip/news.py +235 -0
  17. package/gossip/trader.py +646 -0
  18. package/main.py +287 -0
  19. package/package.json +47 -0
  20. package/requirements.txt +7 -0
  21. package/src/cli.mjs +124 -0
  22. package/src/index.mjs +420 -0
  23. package/web/AGENTS.md +5 -0
  24. package/web/CLAUDE.md +1 -0
  25. package/web/README.md +36 -0
  26. package/web/components.json +25 -0
  27. package/web/eslint.config.mjs +18 -0
  28. package/web/next.config.ts +7 -0
  29. package/web/package-lock.json +11626 -0
  30. package/web/package.json +37 -0
  31. package/web/postcss.config.mjs +7 -0
  32. package/web/public/file.svg +1 -0
  33. package/web/public/globe.svg +1 -0
  34. package/web/public/next.svg +1 -0
  35. package/web/public/vercel.svg +1 -0
  36. package/web/public/window.svg +1 -0
  37. package/web/src/app/api/agent/route.ts +77 -0
  38. package/web/src/app/api/agent/stream/route.ts +87 -0
  39. package/web/src/app/api/markets/route.ts +15 -0
  40. package/web/src/app/api/news/live/route.ts +77 -0
  41. package/web/src/app/api/news/reddit/route.ts +118 -0
  42. package/web/src/app/api/news/route.ts +10 -0
  43. package/web/src/app/api/news/tiktok/route.ts +115 -0
  44. package/web/src/app/api/news/truthsocial/route.ts +116 -0
  45. package/web/src/app/api/news/twitter/route.ts +186 -0
  46. package/web/src/app/api/portfolio/route.ts +50 -0
  47. package/web/src/app/api/prices/route.ts +18 -0
  48. package/web/src/app/api/trades/route.ts +10 -0
  49. package/web/src/app/favicon.ico +0 -0
  50. package/web/src/app/globals.css +170 -0
  51. package/web/src/app/layout.tsx +36 -0
  52. package/web/src/app/page.tsx +366 -0
  53. package/web/src/components/AgentLog.tsx +71 -0
  54. package/web/src/components/LiveStream.tsx +394 -0
  55. package/web/src/components/MarketScanner.tsx +111 -0
  56. package/web/src/components/NewsFeed.tsx +561 -0
  57. package/web/src/components/PortfolioStrip.tsx +139 -0
  58. package/web/src/components/PositionsPanel.tsx +219 -0
  59. package/web/src/components/TopBar.tsx +127 -0
  60. package/web/src/components/ui/badge.tsx +52 -0
  61. package/web/src/components/ui/button.tsx +60 -0
  62. package/web/src/components/ui/card.tsx +103 -0
  63. package/web/src/components/ui/scroll-area.tsx +55 -0
  64. package/web/src/components/ui/separator.tsx +25 -0
  65. package/web/src/components/ui/tabs.tsx +82 -0
  66. package/web/src/components/ui/tooltip.tsx +66 -0
  67. package/web/src/lib/db.ts +81 -0
  68. package/web/src/lib/types.ts +130 -0
  69. package/web/src/lib/utils.ts +6 -0
  70. 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 }