@f5-sales-demo/xcsh-stats 19.51.2
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/README.md +82 -0
- package/build.ts +84 -0
- package/package.json +89 -0
- package/src/aggregator.ts +129 -0
- package/src/client/App.tsx +116 -0
- package/src/client/api.ts +33 -0
- package/src/client/components/ChartsContainer.tsx +219 -0
- package/src/client/components/Header.tsx +48 -0
- package/src/client/components/ModelsTable.tsx +370 -0
- package/src/client/components/RequestDetail.tsx +174 -0
- package/src/client/components/RequestList.tsx +73 -0
- package/src/client/components/StatsGrid.tsx +97 -0
- package/src/client/css.d.ts +1 -0
- package/src/client/index.tsx +6 -0
- package/src/client/styles.css +306 -0
- package/src/client/types.ts +102 -0
- package/src/client/useSystemTheme.ts +31 -0
- package/src/db.ts +482 -0
- package/src/embedded-client.generated.txt +7 -0
- package/src/index.ts +153 -0
- package/src/parser.ts +169 -0
- package/src/server.ts +300 -0
- package/src/types.ts +175 -0
- package/tailwind.config.js +40 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Clock, Coins, FileJson, Gauge, Hash, Star, X, Zap } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { getRequestDetails } from "../api";
|
|
4
|
+
import type { RequestDetails } from "../types";
|
|
5
|
+
|
|
6
|
+
interface RequestDetailProps {
|
|
7
|
+
id: number;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function RequestDetail({ id, onClose }: RequestDetailProps) {
|
|
12
|
+
const [details, setDetails] = useState<RequestDetails | null>(null);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
getRequestDetails(id)
|
|
17
|
+
.then(setDetails)
|
|
18
|
+
.catch(console.error)
|
|
19
|
+
.finally(() => setLoading(false));
|
|
20
|
+
}, [id]);
|
|
21
|
+
|
|
22
|
+
if (!details && loading) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="fixed inset-0 bg-[var(--bg-overlay)] flex justify-center items-center z-[100]">
|
|
25
|
+
<div className="surface px-8 py-6">
|
|
26
|
+
<div className="flex items-center gap-3 text-[var(--text-secondary)]">
|
|
27
|
+
<div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
|
|
28
|
+
<span>Loading...</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!details) return null;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop dismissal
|
|
39
|
+
<div
|
|
40
|
+
role="presentation"
|
|
41
|
+
className="fixed inset-0 bg-[var(--bg-overlay)] backdrop-blur-sm flex justify-end z-[100] animate-fade-in"
|
|
42
|
+
onClick={onClose}
|
|
43
|
+
>
|
|
44
|
+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation for modal content */}
|
|
45
|
+
<div
|
|
46
|
+
role="dialog"
|
|
47
|
+
aria-modal="true"
|
|
48
|
+
className="w-[600px] max-w-full bg-[var(--bg-page)] h-full overflow-y-auto border-l border-[var(--border-subtle)] animate-slide-up"
|
|
49
|
+
onClick={e => e.stopPropagation()}
|
|
50
|
+
>
|
|
51
|
+
{/* Header */}
|
|
52
|
+
<div className="sticky top-0 bg-[var(--bg-page)]/95 backdrop-blur border-b border-[var(--border-subtle)] px-6 py-4 flex justify-between items-center z-10">
|
|
53
|
+
<div className="flex items-center gap-3">
|
|
54
|
+
<div className="w-8 h-8 rounded-[var(--radius-sm)] bg-gradient-to-br from-[var(--accent-pink)]/20 to-[var(--accent-cyan)]/20 flex items-center justify-center">
|
|
55
|
+
<FileJson size={16} className="text-[var(--accent-cyan)]" />
|
|
56
|
+
</div>
|
|
57
|
+
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Request Details</h2>
|
|
58
|
+
</div>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={onClose}
|
|
62
|
+
className="p-2 rounded-[var(--radius-sm)] text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
|
|
63
|
+
>
|
|
64
|
+
<X size={20} />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="p-6 space-y-6">
|
|
69
|
+
{/* Model Info */}
|
|
70
|
+
<div className="surface p-5">
|
|
71
|
+
<div className="flex items-center justify-between mb-4">
|
|
72
|
+
<div>
|
|
73
|
+
<div className="text-2xl font-bold text-[var(--text-primary)]">{details.model}</div>
|
|
74
|
+
<div className="text-sm text-[var(--text-muted)]">{details.provider}</div>
|
|
75
|
+
</div>
|
|
76
|
+
{details.errorMessage ? (
|
|
77
|
+
<span className="badge badge-error">Error</span>
|
|
78
|
+
) : (
|
|
79
|
+
<span className="badge badge-success">Success</span>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Stats Grid */}
|
|
85
|
+
<div className="grid grid-cols-2 gap-4">
|
|
86
|
+
<div className="surface p-4">
|
|
87
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
88
|
+
<Coins size={14} />
|
|
89
|
+
<span className="text-xs uppercase tracking-wide">Cost</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
92
|
+
${details.usage.cost.total.toFixed(4)}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="surface p-4">
|
|
97
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
98
|
+
<Star size={14} />
|
|
99
|
+
<span className="text-xs uppercase tracking-wide">Premium Reqs</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
102
|
+
{(details.usage.premiumRequests ?? 0).toLocaleString()}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="surface p-4">
|
|
106
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
107
|
+
<Hash size={14} />
|
|
108
|
+
<span className="text-xs uppercase tracking-wide">Tokens</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
111
|
+
{details.usage.totalTokens.toLocaleString()}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="text-xs text-[var(--text-muted)] mt-1">
|
|
114
|
+
{details.usage.input.toLocaleString()} in · {details.usage.output.toLocaleString()} out
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="surface p-4">
|
|
119
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
120
|
+
<Clock size={14} />
|
|
121
|
+
<span className="text-xs uppercase tracking-wide">Duration</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
124
|
+
{details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="surface p-4">
|
|
129
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
130
|
+
<Zap size={14} />
|
|
131
|
+
<span className="text-xs uppercase tracking-wide">TTFT</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
134
|
+
{details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Tokens/Sec */}
|
|
140
|
+
{details.duration && details.usage.output > 0 && (
|
|
141
|
+
<div className="surface p-4">
|
|
142
|
+
<div className="flex items-center justify-between">
|
|
143
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)]">
|
|
144
|
+
<Gauge size={14} />
|
|
145
|
+
<span className="text-xs uppercase tracking-wide">Throughput</span>
|
|
146
|
+
</div>
|
|
147
|
+
<span className="text-2xl font-bold gradient-text">
|
|
148
|
+
{((details.usage.output * 1000) / details.duration).toFixed(1)}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="text-xs text-[var(--text-muted)] mt-1 text-right">tokens/second</div>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Output */}
|
|
156
|
+
<div>
|
|
157
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Output</h3>
|
|
158
|
+
<pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-sm font-mono text-[var(--text-secondary)] overflow-x-auto">
|
|
159
|
+
{JSON.stringify(details.output, null, 2)}
|
|
160
|
+
</pre>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Raw Metadata */}
|
|
164
|
+
<div>
|
|
165
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Raw Metadata</h3>
|
|
166
|
+
<pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-xs font-mono text-[var(--text-muted)] overflow-x-auto">
|
|
167
|
+
{JSON.stringify(details, null, 2)}
|
|
168
|
+
</pre>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { formatDistanceToNow } from "date-fns";
|
|
2
|
+
import { CheckCircle2, XCircle } from "lucide-react";
|
|
3
|
+
import type { MessageStats } from "../types";
|
|
4
|
+
|
|
5
|
+
interface RequestListProps {
|
|
6
|
+
requests: MessageStats[];
|
|
7
|
+
onSelect: (req: MessageStats) => void;
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function RequestList({ requests, onSelect, title }: RequestListProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="surface overflow-hidden flex flex-col h-full">
|
|
14
|
+
<div className="px-5 py-4 border-b border-[var(--border-subtle)]">
|
|
15
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)]">{title}</h3>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="overflow-auto flex-1">
|
|
18
|
+
<table className="w-full">
|
|
19
|
+
<thead className="bg-[var(--bg-elevated)] sticky top-0 z-10">
|
|
20
|
+
<tr>
|
|
21
|
+
<th className="text-left py-3 px-4 table-header">Model</th>
|
|
22
|
+
<th className="text-left py-3 px-4 table-header">Time</th>
|
|
23
|
+
<th className="text-right py-3 px-4 table-header">Tokens</th>
|
|
24
|
+
<th className="text-right py-3 px-4 table-header">Cost</th>
|
|
25
|
+
<th className="text-right py-3 px-4 table-header">Duration</th>
|
|
26
|
+
<th className="text-center py-3 px-4 table-header">Status</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
{requests.map(req => (
|
|
31
|
+
<tr
|
|
32
|
+
key={`${req.sessionFile}-${req.entryId}`}
|
|
33
|
+
onClick={() => onSelect(req)}
|
|
34
|
+
className="table-row cursor-pointer border-b border-[var(--border-subtle)] last:border-b-0"
|
|
35
|
+
>
|
|
36
|
+
<td className="py-3 px-4">
|
|
37
|
+
<div className="font-medium text-[var(--text-primary)] text-sm">{req.model}</div>
|
|
38
|
+
<div className="text-xs text-[var(--text-muted)]">{req.provider}</div>
|
|
39
|
+
</td>
|
|
40
|
+
<td className="py-3 px-4 text-sm text-[var(--text-secondary)]">
|
|
41
|
+
{formatDistanceToNow(req.timestamp, { addSuffix: true })}
|
|
42
|
+
</td>
|
|
43
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
44
|
+
{req.usage.totalTokens.toLocaleString()}
|
|
45
|
+
</td>
|
|
46
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
47
|
+
${req.usage.cost.total.toFixed(4)}
|
|
48
|
+
</td>
|
|
49
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
50
|
+
{req.duration ? `${(req.duration / 1000).toFixed(1)}s` : "-"}
|
|
51
|
+
</td>
|
|
52
|
+
<td className="py-3 px-4 text-center">
|
|
53
|
+
{req.errorMessage ? (
|
|
54
|
+
<XCircle size={16} className="text-[var(--accent-red)] mx-auto" />
|
|
55
|
+
) : (
|
|
56
|
+
<CheckCircle2 size={16} className="text-[var(--accent-green)] mx-auto" />
|
|
57
|
+
)}
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
))}
|
|
61
|
+
{requests.length === 0 && (
|
|
62
|
+
<tr>
|
|
63
|
+
<td colSpan={6} className="py-12 text-center text-[var(--text-muted)] text-sm">
|
|
64
|
+
No requests found
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
)}
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Activity, AlertCircle, BarChart3, Database, Server, Star, Zap } from "lucide-react";
|
|
2
|
+
import type { AggregatedStats } from "../types";
|
|
3
|
+
|
|
4
|
+
interface StatsGridProps {
|
|
5
|
+
stats: AggregatedStats;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const statConfig = [
|
|
9
|
+
{
|
|
10
|
+
key: "requests",
|
|
11
|
+
title: "Total Requests",
|
|
12
|
+
icon: Server,
|
|
13
|
+
color: "var(--accent-violet)",
|
|
14
|
+
getValue: (s: AggregatedStats) => s.totalRequests.toLocaleString(),
|
|
15
|
+
getDetail: (s: AggregatedStats) =>
|
|
16
|
+
`${s.successfulRequests.toLocaleString()} success · ${s.failedRequests.toLocaleString()} errors`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "cost",
|
|
20
|
+
title: "Total Cost",
|
|
21
|
+
icon: Activity,
|
|
22
|
+
color: "var(--accent-pink)",
|
|
23
|
+
getValue: (s: AggregatedStats) => `$${s.totalCost.toFixed(2)}`,
|
|
24
|
+
getDetail: (s: AggregatedStats) =>
|
|
25
|
+
s.totalRequests > 0 ? `$${(s.totalCost / s.totalRequests).toFixed(4)} avg/req` : "-",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "premiumRequests",
|
|
29
|
+
title: "Premium Reqs",
|
|
30
|
+
icon: Star,
|
|
31
|
+
color: "var(--accent-amber)",
|
|
32
|
+
getValue: (s: AggregatedStats) => s.totalPremiumRequests.toLocaleString(),
|
|
33
|
+
getDetail: (s: AggregatedStats) =>
|
|
34
|
+
s.totalRequests > 0 ? `${((s.totalPremiumRequests / s.totalRequests) * 100).toFixed(1)}% of requests` : "-",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "cache",
|
|
38
|
+
title: "Cache Rate",
|
|
39
|
+
icon: Database,
|
|
40
|
+
color: "var(--accent-cyan)",
|
|
41
|
+
getValue: (s: AggregatedStats) => `${(s.cacheRate * 100).toFixed(1)}%`,
|
|
42
|
+
getDetail: (s: AggregatedStats) => `${(s.totalCacheReadTokens / 1000).toFixed(1)}k cached tokens`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "errors",
|
|
46
|
+
title: "Error Rate",
|
|
47
|
+
icon: AlertCircle,
|
|
48
|
+
color: "var(--accent-red)",
|
|
49
|
+
getValue: (s: AggregatedStats) => `${(s.errorRate * 100).toFixed(1)}%`,
|
|
50
|
+
getDetail: (s: AggregatedStats) => `${s.failedRequests.toLocaleString()} failed requests`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "tokens",
|
|
54
|
+
title: "Tokens/Sec",
|
|
55
|
+
icon: BarChart3,
|
|
56
|
+
color: "var(--accent-green)",
|
|
57
|
+
getValue: (s: AggregatedStats) => s.avgTokensPerSecond?.toFixed(1) ?? "-",
|
|
58
|
+
getDetail: (s: AggregatedStats) => `${(s.totalInputTokens + s.totalOutputTokens).toLocaleString()} total tokens`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "ttft",
|
|
62
|
+
title: "TTFT",
|
|
63
|
+
icon: Zap,
|
|
64
|
+
color: "var(--accent-amber)",
|
|
65
|
+
getValue: (s: AggregatedStats) => (s.avgTtft ? `${(s.avgTtft / 1000).toFixed(2)}s` : "-"),
|
|
66
|
+
getDetail: () => "Time to first token",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export function StatsGrid({ stats }: StatsGridProps) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-7 gap-4 mb-8">
|
|
73
|
+
{statConfig.map(stat => {
|
|
74
|
+
const Icon = stat.icon;
|
|
75
|
+
return (
|
|
76
|
+
<div key={stat.key} className="stat-card group">
|
|
77
|
+
<div className="flex items-center justify-between mb-3">
|
|
78
|
+
<span className="text-sm font-medium text-[var(--text-secondary)]">{stat.title}</span>
|
|
79
|
+
<div
|
|
80
|
+
className="p-2 rounded-[var(--radius-sm)] transition-colors"
|
|
81
|
+
style={{ backgroundColor: `${stat.color}15` }}
|
|
82
|
+
>
|
|
83
|
+
<Icon
|
|
84
|
+
size={18}
|
|
85
|
+
style={{ color: stat.color }}
|
|
86
|
+
className="transition-transform group-hover:scale-110"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="text-2xl font-bold text-[var(--text-primary)] mb-1">{stat.getValue(stats)}</div>
|
|
91
|
+
<div className="text-xs text-[var(--text-muted)] truncate">{stat.getDetail(stats)}</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
@import "tailwindcss/index.css";
|
|
2
|
+
:root {
|
|
3
|
+
color-scheme: light;
|
|
4
|
+
--bg-page: #f8fafc;
|
|
5
|
+
--bg-surface: #ffffff;
|
|
6
|
+
--bg-elevated: #f1f5f9;
|
|
7
|
+
--bg-hover: rgba(15, 23, 42, 0.04);
|
|
8
|
+
--bg-active: rgba(15, 23, 42, 0.08);
|
|
9
|
+
|
|
10
|
+
--border-subtle: rgba(15, 23, 42, 0.08);
|
|
11
|
+
--border-default: rgba(15, 23, 42, 0.14);
|
|
12
|
+
|
|
13
|
+
--text-primary: #0f172a;
|
|
14
|
+
--text-secondary: #334155;
|
|
15
|
+
--text-muted: #64748b;
|
|
16
|
+
|
|
17
|
+
--accent-pink: #ec4899;
|
|
18
|
+
--accent-pink-glow: rgba(236, 72, 153, 0.26);
|
|
19
|
+
--accent-cyan: #0891b2;
|
|
20
|
+
--accent-cyan-glow: rgba(8, 145, 178, 0.24);
|
|
21
|
+
--accent-violet: #8b5cf6;
|
|
22
|
+
--accent-green: #16a34a;
|
|
23
|
+
--accent-amber: #d97706;
|
|
24
|
+
--accent-red: #dc2626;
|
|
25
|
+
|
|
26
|
+
--tab-active-highlight: inset 0 1px 0 rgba(15, 23, 42, 0.08);
|
|
27
|
+
|
|
28
|
+
--radius-sm: 6px;
|
|
29
|
+
--radius-md: 10px;
|
|
30
|
+
--radius-lg: 14px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root {
|
|
35
|
+
color-scheme: dark;
|
|
36
|
+
--bg-page: #0a0a0f;
|
|
37
|
+
--bg-surface: #111118;
|
|
38
|
+
--bg-elevated: #16161e;
|
|
39
|
+
--bg-hover: rgba(255, 255, 255, 0.03);
|
|
40
|
+
--bg-active: rgba(255, 255, 255, 0.06);
|
|
41
|
+
|
|
42
|
+
--border-subtle: rgba(255, 255, 255, 0.06);
|
|
43
|
+
--border-default: rgba(255, 255, 255, 0.1);
|
|
44
|
+
|
|
45
|
+
--text-primary: #f8fafc;
|
|
46
|
+
--text-secondary: #94a3b8;
|
|
47
|
+
--text-muted: #64748b;
|
|
48
|
+
|
|
49
|
+
--accent-pink: #ec4899;
|
|
50
|
+
--accent-pink-glow: rgba(236, 72, 153, 0.3);
|
|
51
|
+
--accent-cyan: #22d3ee;
|
|
52
|
+
--accent-cyan-glow: rgba(34, 211, 238, 0.3);
|
|
53
|
+
--accent-violet: #a78bfa;
|
|
54
|
+
--accent-green: #4ade80;
|
|
55
|
+
--accent-amber: #fbbf24;
|
|
56
|
+
--accent-red: #f87171;
|
|
57
|
+
|
|
58
|
+
--tab-active-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@layer base {
|
|
63
|
+
* {
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
html {
|
|
68
|
+
-webkit-font-smoothing: antialiased;
|
|
69
|
+
-moz-osx-font-smoothing: grayscale;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
body {
|
|
73
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
74
|
+
background: var(--bg-page);
|
|
75
|
+
color: var(--text-primary);
|
|
76
|
+
margin: 0;
|
|
77
|
+
min-height: 100vh;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Custom scrollbar */
|
|
81
|
+
::-webkit-scrollbar {
|
|
82
|
+
width: 8px;
|
|
83
|
+
height: 8px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
::-webkit-scrollbar-track {
|
|
87
|
+
background: transparent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
::-webkit-scrollbar-thumb {
|
|
91
|
+
background: var(--border-default);
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
::-webkit-scrollbar-thumb:hover {
|
|
96
|
+
background: var(--text-muted);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@layer components {
|
|
101
|
+
.surface {
|
|
102
|
+
background: var(--bg-surface);
|
|
103
|
+
border: 1px solid var(--border-subtle);
|
|
104
|
+
border-radius: var(--radius-lg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.surface-elevated {
|
|
108
|
+
background: var(--bg-elevated);
|
|
109
|
+
border: 1px solid var(--border-default);
|
|
110
|
+
border-radius: var(--radius-lg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.btn {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
gap: 8px;
|
|
118
|
+
padding: 8px 16px;
|
|
119
|
+
font-size: 14px;
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
border-radius: var(--radius-md);
|
|
122
|
+
border: none;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: all 0.15s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.btn-primary {
|
|
128
|
+
background: linear-gradient(135deg, var(--accent-pink) 0%, #db2777 100%);
|
|
129
|
+
color: white;
|
|
130
|
+
box-shadow: 0 0 20px var(--accent-pink-glow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.btn-primary:hover {
|
|
134
|
+
transform: translateY(-1px);
|
|
135
|
+
box-shadow: 0 4px 24px var(--accent-pink-glow);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.btn-primary:disabled {
|
|
139
|
+
opacity: 0.6;
|
|
140
|
+
cursor: not-allowed;
|
|
141
|
+
transform: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.btn-secondary {
|
|
145
|
+
background: var(--bg-elevated);
|
|
146
|
+
color: var(--text-primary);
|
|
147
|
+
border: 1px solid var(--border-default);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.btn-secondary:hover {
|
|
151
|
+
background: var(--bg-hover);
|
|
152
|
+
border-color: var(--border-default);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.tab-btn {
|
|
156
|
+
padding: 6px 16px;
|
|
157
|
+
font-size: 14px;
|
|
158
|
+
font-weight: 500;
|
|
159
|
+
color: var(--text-muted);
|
|
160
|
+
background: transparent;
|
|
161
|
+
border: none;
|
|
162
|
+
border-radius: var(--radius-md);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
transition: all 0.15s ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.tab-btn:hover {
|
|
168
|
+
color: var(--text-primary);
|
|
169
|
+
background: var(--bg-hover);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.tab-btn.active {
|
|
173
|
+
color: var(--text-primary);
|
|
174
|
+
background: var(--bg-elevated);
|
|
175
|
+
box-shadow: var(--tab-active-highlight);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.stat-card {
|
|
179
|
+
background: var(--bg-surface);
|
|
180
|
+
border: 1px solid var(--border-subtle);
|
|
181
|
+
border-radius: var(--radius-lg);
|
|
182
|
+
padding: 20px;
|
|
183
|
+
transition: all 0.2s ease;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.stat-card:hover {
|
|
187
|
+
border-color: var(--border-default);
|
|
188
|
+
background: var(--bg-elevated);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.badge {
|
|
192
|
+
display: inline-flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
padding: 3px 10px;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
font-weight: 500;
|
|
197
|
+
border-radius: 100px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.badge-success {
|
|
201
|
+
background: rgba(74, 222, 128, 0.15);
|
|
202
|
+
color: var(--accent-green);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.badge-error {
|
|
206
|
+
background: rgba(248, 113, 113, 0.15);
|
|
207
|
+
color: var(--accent-red);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.badge-info {
|
|
211
|
+
background: rgba(34, 211, 238, 0.15);
|
|
212
|
+
color: var(--accent-cyan);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.badge-warning {
|
|
216
|
+
background: rgba(251, 191, 36, 0.15);
|
|
217
|
+
color: var(--accent-amber);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.table-header {
|
|
221
|
+
font-size: 11px;
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
text-transform: uppercase;
|
|
224
|
+
letter-spacing: 0.05em;
|
|
225
|
+
color: var(--text-muted);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.table-row {
|
|
229
|
+
transition: background 0.15s ease;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.table-row:hover {
|
|
233
|
+
background: var(--bg-hover);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.gradient-text {
|
|
237
|
+
background: linear-gradient(135deg, var(--accent-pink) 0%, var(--accent-cyan) 100%);
|
|
238
|
+
-webkit-background-clip: text;
|
|
239
|
+
-webkit-text-fill-color: transparent;
|
|
240
|
+
background-clip: text;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.gradient-border {
|
|
244
|
+
position: relative;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.gradient-border::before {
|
|
248
|
+
content: '';
|
|
249
|
+
position: absolute;
|
|
250
|
+
inset: 0;
|
|
251
|
+
padding: 1px;
|
|
252
|
+
border-radius: inherit;
|
|
253
|
+
background: linear-gradient(135deg, var(--accent-pink), var(--accent-cyan));
|
|
254
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
255
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
256
|
+
-webkit-mask-composite: xor;
|
|
257
|
+
mask-composite: exclude;
|
|
258
|
+
pointer-events: none;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.glow-pink {
|
|
262
|
+
box-shadow: 0 0 30px var(--accent-pink-glow);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.glow-cyan {
|
|
266
|
+
box-shadow: 0 0 30px var(--accent-cyan-glow);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@layer utilities {
|
|
271
|
+
.text-balance {
|
|
272
|
+
text-wrap: balance;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.animate-fade-in {
|
|
276
|
+
animation: fadeIn 0.3s ease-out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.animate-slide-up {
|
|
280
|
+
animation: slideUp 0.3s ease-out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@keyframes fadeIn {
|
|
284
|
+
from { opacity: 0; }
|
|
285
|
+
to { opacity: 1; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@keyframes slideUp {
|
|
289
|
+
from {
|
|
290
|
+
opacity: 0;
|
|
291
|
+
transform: translateY(10px);
|
|
292
|
+
}
|
|
293
|
+
to {
|
|
294
|
+
opacity: 1;
|
|
295
|
+
transform: translateY(0);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.spin {
|
|
300
|
+
animation: spin 1s linear infinite;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@keyframes spin {
|
|
304
|
+
to { transform: rotate(360deg); }
|
|
305
|
+
}
|
|
306
|
+
}
|