@arraystar/tokenscope 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 (45) hide show
  1. package/README.md +87 -0
  2. package/bun.lock +1170 -0
  3. package/components.json +25 -0
  4. package/eslint.config.js +23 -0
  5. package/index.html +13 -0
  6. package/package.json +46 -0
  7. package/public/data.json +41 -0
  8. package/public/favicon.svg +1 -0
  9. package/public/icons.svg +24 -0
  10. package/src/App.css +184 -0
  11. package/src/App.tsx +98 -0
  12. package/src/PricingContext.tsx +131 -0
  13. package/src/assets/hero.png +0 -0
  14. package/src/assets/react.svg +1 -0
  15. package/src/assets/vite.svg +1 -0
  16. package/src/cli.ts +98 -0
  17. package/src/components/Overview.tsx +468 -0
  18. package/src/components/PricingSheet.tsx +209 -0
  19. package/src/components/SessionDetail.tsx +244 -0
  20. package/src/components/ui/badge.tsx +52 -0
  21. package/src/components/ui/button.tsx +58 -0
  22. package/src/components/ui/card.tsx +103 -0
  23. package/src/components/ui/input.tsx +20 -0
  24. package/src/components/ui/separator.tsx +23 -0
  25. package/src/components/ui/sheet.tsx +138 -0
  26. package/src/components/ui/sidebar.tsx +721 -0
  27. package/src/components/ui/skeleton.tsx +13 -0
  28. package/src/components/ui/table.tsx +114 -0
  29. package/src/components/ui/tooltip.tsx +64 -0
  30. package/src/data.ts +90 -0
  31. package/src/hooks/use-mobile.ts +19 -0
  32. package/src/i18n.tsx +148 -0
  33. package/src/index.css +138 -0
  34. package/src/lib/utils.ts +6 -0
  35. package/src/main.tsx +10 -0
  36. package/src/parser/claude.ts +225 -0
  37. package/src/parser/codex.ts +181 -0
  38. package/src/parser/index.ts +83 -0
  39. package/src/parser/types.ts +35 -0
  40. package/src/pricing.ts +139 -0
  41. package/src/types.ts +56 -0
  42. package/tsconfig.app.json +30 -0
  43. package/tsconfig.json +13 -0
  44. package/tsconfig.node.json +26 -0
  45. package/vite.config.ts +13 -0
@@ -0,0 +1,209 @@
1
+ import { usePricingConfig } from "../PricingContext";
2
+ import { PRICING_DB, findPricingEntry, type CustomPrice } from "../pricing";
3
+ import { useLang, useTranslations } from "../i18n";
4
+ import { X, RotateCcw } from "lucide-react";
5
+ import { Button } from "./ui/button";
6
+ import { useState } from "react";
7
+
8
+ export default function PricingSheet() {
9
+ const { config, detectedModels, isOpen, setOpen, updateModelMapping, updateCustomPricing, resetConfig } =
10
+ usePricingConfig();
11
+ const { lang } = useLang();
12
+ const tr = useTranslations(lang);
13
+
14
+ if (!isOpen) return null;
15
+
16
+ return (
17
+ <>
18
+ {/* Backdrop */}
19
+ <div
20
+ className="fixed inset-0 bg-black/40 z-40 transition-opacity"
21
+ onClick={() => setOpen(false)}
22
+ />
23
+ {/* Panel */}
24
+ <div className="fixed right-0 top-0 bottom-0 w-[420px] max-w-full bg-background border-l border-border z-50 shadow-xl flex flex-col animate-slide-in">
25
+ {/* Header */}
26
+ <div className="flex items-center justify-between px-5 py-4 border-b border-border">
27
+ <h2 className="text-base font-semibold">{tr.modelPricing}</h2>
28
+ <Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
29
+ <X className="w-4 h-4" />
30
+ </Button>
31
+ </div>
32
+
33
+ {/* Body */}
34
+ <div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
35
+ <p className="text-xs text-muted-foreground">
36
+ {tr.detectedModels}: {detectedModels.length}
37
+ </p>
38
+
39
+ {detectedModels.map((modelName) => (
40
+ <ModelConfigRow
41
+ key={modelName}
42
+ modelName={modelName}
43
+ mapping={config.modelMappings[modelName]}
44
+ custom={config.customPricing[modelName]}
45
+ onMappingChange={(id) => updateModelMapping(modelName, id)}
46
+ onCustomChange={(cp) => updateCustomPricing(modelName, cp)}
47
+ />
48
+ ))}
49
+ </div>
50
+
51
+ {/* Footer */}
52
+ <div className="px-5 py-3 border-t border-border">
53
+ <Button variant="outline" size="sm" onClick={resetConfig} className="w-full gap-2">
54
+ <RotateCcw className="w-3 h-3" />
55
+ {tr.resetToDefaults}
56
+ </Button>
57
+ </div>
58
+ </div>
59
+ </>
60
+ );
61
+ }
62
+
63
+ function ModelConfigRow({
64
+ modelName,
65
+ mapping,
66
+ custom,
67
+ onMappingChange,
68
+ onCustomChange,
69
+ }: {
70
+ modelName: string;
71
+ mapping?: string;
72
+ custom?: CustomPrice;
73
+ onMappingChange: (id: string | null) => void;
74
+ onCustomChange: (cp: CustomPrice | null) => void;
75
+ }) {
76
+ const { lang } = useLang();
77
+ const tr = useTranslations(lang);
78
+ const [isCustom, setIsCustom] = useState(!!custom);
79
+ const [customInput, setCustomInput] = useState(custom?.input ?? 0);
80
+ const [customOutput, setCustomOutput] = useState(custom?.output ?? 0);
81
+ const [customCacheRead, setCustomCacheRead] = useState(custom?.cacheRead ?? 0);
82
+ const [customCurrency, setCustomCurrency] = useState<"usd" | "cny">(custom?.currency ?? "usd");
83
+
84
+ const currentEntry = mapping ? findPricingEntry(mapping) : undefined;
85
+
86
+ const handleSelectChange = (value: string) => {
87
+ if (value === "__custom__") {
88
+ setIsCustom(true);
89
+ } else {
90
+ setIsCustom(false);
91
+ onMappingChange(value || null);
92
+ onCustomChange(null);
93
+ }
94
+ };
95
+
96
+ const handleCustomApply = () => {
97
+ onCustomChange({
98
+ input: customInput,
99
+ output: customOutput,
100
+ cacheRead: customCacheRead,
101
+ currency: customCurrency,
102
+ });
103
+ };
104
+
105
+ // Group pricing entries by provider
106
+ const providers = PRICING_DB.reduce<Record<string, typeof PRICING_DB>>((acc, entry) => {
107
+ if (!acc[entry.provider]) acc[entry.provider] = [];
108
+ acc[entry.provider].push(entry);
109
+ return acc;
110
+ }, {});
111
+
112
+ const curSymbol = (c: string) => (c === "cny" ? "¥" : "$");
113
+
114
+ return (
115
+ <div className="space-y-2.5 rounded-lg border border-border p-3">
116
+ {/* Model name + current pricing preview */}
117
+ <div className="flex items-center justify-between">
118
+ <span className="text-sm font-semibold font-mono">{modelName}</span>
119
+ {currentEntry && (
120
+ <span className="text-[10px] text-muted-foreground">
121
+ {curSymbol(currentEntry.currency)}
122
+ {currentEntry.input}/{curSymbol(currentEntry.currency)}
123
+ {currentEntry.output}/{curSymbol(currentEntry.currency)}
124
+ {currentEntry.cacheRead}
125
+ </span>
126
+ )}
127
+ {custom && (
128
+ <span className="text-[10px] text-muted-foreground">
129
+ {curSymbol(custom.currency)}
130
+ {custom.input}/{curSymbol(custom.currency)}
131
+ {custom.output}/{curSymbol(custom.currency)}
132
+ {custom.cacheRead}
133
+ </span>
134
+ )}
135
+ </div>
136
+
137
+ {/* Pricing selector */}
138
+ <select
139
+ value={isCustom ? "__custom__" : (mapping ?? "")}
140
+ onChange={(e) => handleSelectChange(e.target.value)}
141
+ className="w-full bg-background border border-border rounded-md px-2.5 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
142
+ >
143
+ <option value="">{tr.noPricing}</option>
144
+ {Object.entries(providers).map(([provider, entries]) => (
145
+ <optgroup key={provider} label={provider}>
146
+ {entries.map((entry) => (
147
+ <option key={entry.id} value={entry.id}>
148
+ {entry.model} ({curSymbol(entry.currency)}{entry.input}/{curSymbol(entry.currency)}{entry.output})
149
+ </option>
150
+ ))}
151
+ </optgroup>
152
+ ))}
153
+ <option value="__custom__">{tr.customPricing}</option>
154
+ </select>
155
+
156
+ {/* Custom pricing inputs */}
157
+ {isCustom && (
158
+ <div className="space-y-2 pt-1">
159
+ <div className="grid grid-cols-3 gap-2">
160
+ <div>
161
+ <label className="text-[10px] text-muted-foreground">{tr.inputPrice}</label>
162
+ <input
163
+ type="number"
164
+ step="0.01"
165
+ value={customInput}
166
+ onChange={(e) => setCustomInput(Number(e.target.value))}
167
+ className="w-full bg-background border border-border rounded px-2 py-1 text-xs font-mono"
168
+ />
169
+ </div>
170
+ <div>
171
+ <label className="text-[10px] text-muted-foreground">{tr.outputPrice}</label>
172
+ <input
173
+ type="number"
174
+ step="0.01"
175
+ value={customOutput}
176
+ onChange={(e) => setCustomOutput(Number(e.target.value))}
177
+ className="w-full bg-background border border-border rounded px-2 py-1 text-xs font-mono"
178
+ />
179
+ </div>
180
+ <div>
181
+ <label className="text-[10px] text-muted-foreground">{tr.cacheReadPrice}</label>
182
+ <input
183
+ type="number"
184
+ step="0.01"
185
+ value={customCacheRead}
186
+ onChange={(e) => setCustomCacheRead(Number(e.target.value))}
187
+ className="w-full bg-background border border-border rounded px-2 py-1 text-xs font-mono"
188
+ />
189
+ </div>
190
+ </div>
191
+ <div className="flex items-center gap-2">
192
+ <select
193
+ value={customCurrency}
194
+ onChange={(e) => setCustomCurrency(e.target.value as "usd" | "cny")}
195
+ className="bg-background border border-border rounded px-2 py-1 text-xs"
196
+ >
197
+ <option value="usd">USD ($)</option>
198
+ <option value="cny">CNY (¥)</option>
199
+ </select>
200
+ <span className="text-[10px] text-muted-foreground">{tr.perMillionTokens}</span>
201
+ <Button size="sm" variant="secondary" onClick={handleCustomApply} className="ml-auto text-xs h-7">
202
+ Apply
203
+ </Button>
204
+ </div>
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ }
@@ -0,0 +1,244 @@
1
+ import { useParams, useNavigate } from "react-router-dom";
2
+ import {
3
+ BarChart,
4
+ Bar,
5
+ XAxis,
6
+ YAxis,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ Cell,
10
+ } from "recharts";
11
+ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
12
+ import { Badge } from "./ui/badge";
13
+ import { Button } from "./ui/button";
14
+ import { fmt, fmtCost, getAllSessions } from "../data";
15
+ import { useRecalculatedData, usePricingConfig } from "../PricingContext";
16
+ import { useLang, useTranslations } from "../i18n";
17
+ import { ArrowLeft, MessageSquare, Zap, Database, Coins, ArrowUpDown } from "lucide-react";
18
+ import { useState, useMemo } from "react";
19
+
20
+ export default function SessionDetail() {
21
+ const { sessionKey } = useParams();
22
+ const navigate = useNavigate();
23
+ const data = useRecalculatedData();
24
+ const { currencySymbol } = usePricingConfig();
25
+ const allSessions = getAllSessions(data);
26
+ const { lang } = useLang();
27
+ const tr = useTranslations(lang);
28
+ const [sortBy, setSortBy] = useState<"total" | "time" | "input" | "output" | "cache_read">("total");
29
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
30
+
31
+ const session = allSessions.find((s) => s.sessionKey === decodeURIComponent(sessionKey || ""));
32
+ if (!session) {
33
+ return (
34
+ <div className="text-center py-20">
35
+ <p className="text-muted-foreground">{tr.sessionNotFound}</p>
36
+ <Button variant="ghost" onClick={() => navigate("/")} className="mt-4">
37
+ <ArrowLeft className="w-4 h-4 mr-2" /> {tr.backToOverview}
38
+ </Button>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ const sortedTurns = useMemo(() => {
44
+ const t = [...session.turns];
45
+ const dir = sortDir === "asc" ? 1 : -1;
46
+ t.sort((a, b) => {
47
+ if (sortBy === "time") return a.time.localeCompare(b.time) * dir;
48
+ return (a[sortBy] - b[sortBy]) * dir;
49
+ });
50
+ return t;
51
+ }, [session.turns, sortBy, sortDir]);
52
+
53
+ const toggleSort = (col: typeof sortBy) => {
54
+ if (sortBy === col) {
55
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
56
+ } else {
57
+ setSortBy(col);
58
+ setSortDir("desc");
59
+ }
60
+ };
61
+
62
+ const chartData = session.turns.map((t, i) => ({
63
+ idx: i + 1,
64
+ input: t.input,
65
+ output: t.output,
66
+ cache_read: t.cache_read,
67
+ total: t.total,
68
+ user: t.user.slice(0, 30),
69
+ }));
70
+
71
+ const stats = session.stats;
72
+
73
+ return (
74
+ <div className="space-y-6">
75
+ {/* Back button + header */}
76
+ <div className="flex items-center gap-4">
77
+ <Button variant="ghost" size="sm" onClick={() => navigate("/")}>
78
+ <ArrowLeft className="w-4 h-4" />
79
+ </Button>
80
+ <div className="flex-1">
81
+ <h2 className="text-lg font-bold">{session.project}</h2>
82
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
83
+ <span>{tr.session} {session.sid}</span>
84
+ <span>|</span>
85
+ <span>{session.date}</span>
86
+ <Badge variant="secondary">{stats.msgs} {tr.msgs}</Badge>
87
+ </div>
88
+ </div>
89
+ <Badge variant="outline" className="text-base px-3 py-1">
90
+ {fmt(stats.total)} {tr.tokens}
91
+ </Badge>
92
+ </div>
93
+
94
+ {/* Session Stats Cards */}
95
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
96
+ <MiniStat label={tr.inputCol} value={fmt(stats.input)} icon={<Zap className="w-3 h-3" />} color="text-violet-500" />
97
+ <MiniStat label={tr.outputCol} value={fmt(stats.output)} icon={<MessageSquare className="w-3 h-3" />} color="text-emerald-500" />
98
+ <MiniStat label={tr.cacheRCol} value={fmt(stats.cache_read)} icon={<Database className="w-3 h-3" />} color="text-amber-500" />
99
+ <MiniStat label={tr.cacheWriteLabel} value={fmt(stats.cache_write)} icon={<Database className="w-3 h-3" />} color="text-rose-500" />
100
+ <MiniStat label={tr.costCol} value={fmtCost(stats.cost, currencySymbol)} icon={<Coins className="w-3 h-3" />} color="text-sky-500" />
101
+ </div>
102
+
103
+ {/* Turn Timeline Chart */}
104
+ <Card>
105
+ <CardHeader className="pb-2">
106
+ <CardTitle className="text-sm font-medium">{tr.tokenUsagePerTurn}</CardTitle>
107
+ </CardHeader>
108
+ <CardContent>
109
+ <ResponsiveContainer width="100%" height={200}>
110
+ <BarChart data={chartData}>
111
+ <XAxis dataKey="idx" tick={{ fontSize: 10 }} />
112
+ <YAxis tickFormatter={(v: number) => fmt(v)} tick={{ fontSize: 10 }} />
113
+ <Tooltip
114
+ content={({ active, payload }) => {
115
+ if (!active || !payload?.length) return null;
116
+ const d = payload[0].payload;
117
+ return (
118
+ <div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg text-xs">
119
+ <p className="font-medium mb-1">#{d.idx}</p>
120
+ <p>{tr.inputCol}: {fmt(d.input)}</p>
121
+ <p>{tr.outputCol}: {fmt(d.output)}</p>
122
+ <p>{tr.cacheRCol}: {fmt(d.cache_read)}</p>
123
+ <p className="font-semibold mt-1">{tr.totalLabel}: {fmt(d.total)}</p>
124
+ </div>
125
+ );
126
+ }}
127
+ />
128
+ <Bar dataKey="total" radius={[2, 2, 0, 0]}>
129
+ {chartData.map((_, i) => (
130
+ <Cell
131
+ key={i}
132
+ fill={`hsl(220, ${40 + (i % 3) * 15}%, ${45 + (i % 4) * 8}%)`}
133
+ />
134
+ ))}
135
+ </Bar>
136
+ </BarChart>
137
+ </ResponsiveContainer>
138
+ </CardContent>
139
+ </Card>
140
+
141
+ {/* Turns Table */}
142
+ <Card>
143
+ <CardHeader className="pb-2">
144
+ <CardTitle className="text-sm font-medium">
145
+ {tr.conversationTurns} ({session.turns.length})
146
+ </CardTitle>
147
+ </CardHeader>
148
+ <CardContent>
149
+ <div className="overflow-x-auto">
150
+ <table className="w-full text-sm">
151
+ <thead>
152
+ <tr className="border-b border-border">
153
+ <th className="text-left px-3 py-2 text-xs text-muted-foreground font-medium w-10">{tr.turn}</th>
154
+ <th className="text-left px-3 py-2 text-xs text-muted-foreground font-medium w-16">{tr.time}</th>
155
+ <th className="text-left px-3 py-2 text-xs text-muted-foreground font-medium">{tr.message}</th>
156
+ <SortHeader label={tr.inputCol} col="input" sortBy={sortBy} sortDir={sortDir} onToggle={() => toggleSort("input")} />
157
+ <SortHeader label={tr.outputCol} col="output" sortBy={sortBy} sortDir={sortDir} onToggle={() => toggleSort("output")} />
158
+ <SortHeader label={tr.cacheRCol} col="cache_read" sortBy={sortBy} sortDir={sortDir} onToggle={() => toggleSort("cache_read")} />
159
+ <SortHeader label={tr.totalCol} col="total" sortBy={sortBy} sortDir={sortDir} onToggle={() => toggleSort("total")} />
160
+ <th className="text-right px-3 py-2 text-xs text-muted-foreground font-medium">{tr.costCol}</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody>
164
+ {sortedTurns.map((turn, i) => {
165
+ const time = turn.time ? turn.time.slice(11, 16) : "";
166
+ const msg = turn.user || "(tool call)";
167
+ return (
168
+ <tr
169
+ key={i}
170
+ className="border-b border-border/50 hover:bg-accent/50 transition-colors"
171
+ >
172
+ <td className="px-3 py-1.5 text-xs text-muted-foreground">{i + 1}</td>
173
+ <td className="px-3 py-1.5 text-xs text-muted-foreground font-mono">{time}</td>
174
+ <td className="px-3 py-1.5 max-w-[300px] truncate">{msg}</td>
175
+ <td className="px-3 py-1.5 text-right font-mono text-xs">{fmt(turn.input)}</td>
176
+ <td className="px-3 py-1.5 text-right font-mono text-xs">{fmt(turn.output)}</td>
177
+ <td className="px-3 py-1.5 text-right font-mono text-xs">{fmt(turn.cache_read)}</td>
178
+ <td className="px-3 py-1.5 text-right font-mono text-xs font-semibold">{fmt(turn.total)}</td>
179
+ <td className="px-3 py-1.5 text-right font-mono text-xs">{fmtCost(turn.cost, currencySymbol)}</td>
180
+ </tr>
181
+ );
182
+ })}
183
+ </tbody>
184
+ </table>
185
+ </div>
186
+ </CardContent>
187
+ </Card>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ function MiniStat({
193
+ label,
194
+ value,
195
+ icon,
196
+ color,
197
+ }: {
198
+ label: string;
199
+ value: string;
200
+ icon: React.ReactNode;
201
+ color: string;
202
+ }) {
203
+ return (
204
+ <Card>
205
+ <CardContent className="p-3">
206
+ <div className="flex items-center gap-1.5 mb-1">
207
+ <div className={color}>{icon}</div>
208
+ <span className="text-[11px] text-muted-foreground">{label}</span>
209
+ </div>
210
+ <p className="text-sm font-bold">{value}</p>
211
+ </CardContent>
212
+ </Card>
213
+ );
214
+ }
215
+
216
+ function SortHeader({
217
+ label,
218
+ col,
219
+ sortBy,
220
+ sortDir,
221
+ onToggle,
222
+ }: {
223
+ label: string;
224
+ col: string;
225
+ sortBy: string;
226
+ sortDir: string;
227
+ onToggle: () => void;
228
+ }) {
229
+ const active = sortBy === col;
230
+ return (
231
+ <th
232
+ className="text-right px-3 py-2 text-xs text-muted-foreground font-medium cursor-pointer select-none hover:text-foreground transition-colors"
233
+ onClick={onToggle}
234
+ >
235
+ <span className="inline-flex items-center gap-1">
236
+ {label}
237
+ <ArrowUpDown
238
+ className={`w-3 h-3 ${active ? "text-primary" : "opacity-30"}`}
239
+ style={active && sortDir === "desc" ? { transform: "rotate(180deg)" } : undefined}
240
+ />
241
+ </span>
242
+ </th>
243
+ );
244
+ }
@@ -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
+ }