@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,13 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-muted", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,114 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
6
+ return (
7
+ <div
8
+ data-slot="table-container"
9
+ className="relative w-full overflow-x-auto"
10
+ >
11
+ <table
12
+ data-slot="table"
13
+ className={cn("w-full caption-bottom text-sm", className)}
14
+ {...props}
15
+ />
16
+ </div>
17
+ )
18
+ }
19
+
20
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
21
+ return (
22
+ <thead
23
+ data-slot="table-header"
24
+ className={cn("[&_tr]:border-b", className)}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
31
+ return (
32
+ <tbody
33
+ data-slot="table-body"
34
+ className={cn("[&_tr:last-child]:border-0", className)}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
41
+ return (
42
+ <tfoot
43
+ data-slot="table-footer"
44
+ className={cn(
45
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
54
+ return (
55
+ <tr
56
+ data-slot="table-row"
57
+ className={cn(
58
+ "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
67
+ return (
68
+ <th
69
+ data-slot="table-head"
70
+ className={cn(
71
+ "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
72
+ className
73
+ )}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
80
+ return (
81
+ <td
82
+ data-slot="table-cell"
83
+ className={cn(
84
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
85
+ className
86
+ )}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+
92
+ function TableCaption({
93
+ className,
94
+ ...props
95
+ }: React.ComponentProps<"caption">) {
96
+ return (
97
+ <caption
98
+ data-slot="table-caption"
99
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
100
+ {...props}
101
+ />
102
+ )
103
+ }
104
+
105
+ export {
106
+ Table,
107
+ TableHeader,
108
+ TableBody,
109
+ TableFooter,
110
+ TableHead,
111
+ TableRow,
112
+ TableCell,
113
+ TableCaption,
114
+ }
@@ -0,0 +1,64 @@
1
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function TooltipProvider({
6
+ delay = 0,
7
+ ...props
8
+ }: TooltipPrimitive.Provider.Props) {
9
+ return (
10
+ <TooltipPrimitive.Provider
11
+ data-slot="tooltip-provider"
12
+ delay={delay}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
19
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
20
+ }
21
+
22
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
23
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
24
+ }
25
+
26
+ function TooltipContent({
27
+ className,
28
+ side = "top",
29
+ sideOffset = 4,
30
+ align = "center",
31
+ alignOffset = 0,
32
+ children,
33
+ ...props
34
+ }: TooltipPrimitive.Popup.Props &
35
+ Pick<
36
+ TooltipPrimitive.Positioner.Props,
37
+ "align" | "alignOffset" | "side" | "sideOffset"
38
+ >) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Positioner
42
+ align={align}
43
+ alignOffset={alignOffset}
44
+ side={side}
45
+ sideOffset={sideOffset}
46
+ className="isolate z-50"
47
+ >
48
+ <TooltipPrimitive.Popup
49
+ data-slot="tooltip-content"
50
+ className={cn(
51
+ "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
52
+ className
53
+ )}
54
+ {...props}
55
+ >
56
+ {children}
57
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
58
+ </TooltipPrimitive.Popup>
59
+ </TooltipPrimitive.Positioner>
60
+ </TooltipPrimitive.Portal>
61
+ )
62
+ }
63
+
64
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
package/src/data.ts ADDED
@@ -0,0 +1,90 @@
1
+ import type { DashboardData, SessionData } from "./types";
2
+ import data from "../public/data.json" with { type: "json" };
3
+
4
+ export function useData(): DashboardData {
5
+ return data as DashboardData;
6
+ }
7
+
8
+ export function fmt(n: number): string {
9
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
10
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
11
+ return n.toLocaleString();
12
+ }
13
+
14
+ export function fmtCost(cost: number, symbol = "$"): string {
15
+ if (cost >= 1) return `${symbol}${cost.toFixed(2)}`;
16
+ if (cost >= 0.01) return `${symbol}${cost.toFixed(3)}`;
17
+ return `${symbol}${cost.toFixed(4)}`;
18
+ }
19
+
20
+ export function fmtPercent(value: number, total: number): string {
21
+ if (total === 0) return "0%";
22
+ return `${((value / total) * 100).toFixed(1)}%`;
23
+ }
24
+
25
+ const COLORS = [
26
+ "hsl(220, 70%, 50%)",
27
+ "hsl(160, 60%, 45%)",
28
+ "hsl(30, 80%, 55%)",
29
+ "hsl(280, 60%, 55%)",
30
+ "hsl(0, 70%, 55%)",
31
+ "hsl(190, 70%, 45%)",
32
+ "hsl(45, 80%, 50%)",
33
+ "hsl(330, 60%, 50%)",
34
+ ];
35
+
36
+ export function projectColor(index: number): string {
37
+ return COLORS[index % COLORS.length];
38
+ }
39
+
40
+ export interface SkillStat {
41
+ name: string;
42
+ total: number;
43
+ cost: number;
44
+ turns: number;
45
+ color: string;
46
+ }
47
+
48
+ export function getSkillStats(data: DashboardData): SkillStat[] {
49
+ const map = new Map<string, { total: number; cost: number; turns: number }>();
50
+ for (const modelData of Object.values(data.models)) {
51
+ for (const sessionData of Object.values(modelData.sessions)) {
52
+ for (const turn of sessionData.turns) {
53
+ const m = turn.user.match(/skills\/([^/\s]+)/);
54
+ if (!m) continue;
55
+ const name = m[1];
56
+ const cur = map.get(name) ?? { total: 0, cost: 0, turns: 0 };
57
+ cur.total += turn.total;
58
+ cur.cost += turn.cost;
59
+ cur.turns += 1;
60
+ map.set(name, cur);
61
+ }
62
+ }
63
+ }
64
+ return Array.from(map.entries())
65
+ .map(([name, v], i) => ({ name, ...v, color: COLORS[i % COLORS.length] }))
66
+ .sort((a, b) => b.total - a.total);
67
+ }
68
+
69
+ export function getAllSessions(data: DashboardData): (SessionData & { model: string; sessionKey: string })[] {
70
+ const sessions: (SessionData & { model: string; sessionKey: string })[] = [];
71
+ for (const [modelName, modelData] of Object.entries(data.models)) {
72
+ for (const [sessionKey, sessionData] of Object.entries(modelData.sessions)) {
73
+ sessions.push({ ...sessionData, model: modelName, sessionKey });
74
+ }
75
+ }
76
+ return sessions.sort((a, b) => b.stats.total - a.stats.total);
77
+ }
78
+
79
+ export function getSources(data: DashboardData): ("claude" | "codex")[] {
80
+ if (data.sources?.length) return data.sources;
81
+ // Auto-detect: if any session has source field, collect unique values
82
+ const found = new Set<"claude" | "codex">();
83
+ for (const modelData of Object.values(data.models)) {
84
+ for (const sessionData of Object.values(modelData.sessions)) {
85
+ if (sessionData.source) found.add(sessionData.source);
86
+ }
87
+ }
88
+ return found.size > 0 ? Array.from(found) : ["claude"]; // default to claude for legacy data
89
+ }
90
+
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/i18n.tsx ADDED
@@ -0,0 +1,148 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ export type Lang = "zh" | "en";
4
+
5
+ const t = {
6
+ zh: {
7
+ title: "TokenScope",
8
+ subtitle: "Your token, visualized.",
9
+ overview: "概览",
10
+ sessions: "会话",
11
+ generated: "生成于",
12
+ totalTokens: "总 Tokens",
13
+ messages: "条消息",
14
+ cacheRead: "缓存读取",
15
+ ofTotal: "% 占总量",
16
+ sessionsCount: "会话数",
17
+ projects: "个项目",
18
+ estCost: "预估费用",
19
+ basedOnPricing: "基于 API 定价",
20
+ tokenByProject: "按项目分布",
21
+ tokenBreakdown: "Token 构成",
22
+ tokenBySkill: "按技能分布",
23
+ turns: "轮",
24
+ dailyUsage: "每日 Token 用量",
25
+ topSessions: "热门会话",
26
+ input: "输入",
27
+ output: "输出",
28
+ cacheReadLabel: "缓存读",
29
+ cacheWriteLabel: "缓存写",
30
+ msgs: "条消息",
31
+ sessionNotFound: "未找到该会话",
32
+ backToOverview: "返回总览",
33
+ session: "会话",
34
+ tokens: "tokens",
35
+ tokenUsagePerTurn: "每轮对话 Token 用量",
36
+ conversationTurns: "对话记录",
37
+ turn: "轮",
38
+ time: "时间",
39
+ message: "消息",
40
+ inputCol: "输入",
41
+ outputCol: "输出",
42
+ cacheRCol: "缓存读",
43
+ cacheWriteCol: "缓存写",
44
+ totalCol: "总计",
45
+ costCol: "费用",
46
+ totalLabel: "总计",
47
+ sortAsc: "升序",
48
+ sortDesc: "降序",
49
+ turnLabel: "轮次",
50
+ timeLabel: "时间",
51
+ messageLabel: "消息",
52
+ settings: "设置",
53
+ modelPricing: "模型定价",
54
+ detectedModels: "检测到的模型",
55
+ selectPricing: "选择定价方案",
56
+ customPricing: "自定义",
57
+ inputPrice: "输入价格",
58
+ outputPrice: "输出价格",
59
+ cacheReadPrice: "缓存读价格",
60
+ perMillionTokens: "每百万 tokens",
61
+ resetToDefaults: "恢复默认",
62
+ noPricing: "未设置定价",
63
+ currency: "货币",
64
+ sourceFilter: "数据来源",
65
+ allSources: "全部",
66
+ claudeCode: "Claude Code",
67
+ codexCli: "Codex CLI",
68
+ },
69
+ en: {
70
+ title: "TokenScope",
71
+ subtitle: "Your token, visualized.",
72
+ overview: "Overview",
73
+ sessions: "Sessions",
74
+ generated: "Generated",
75
+ totalTokens: "Total Tokens",
76
+ messages: "messages",
77
+ cacheRead: "Cache Read",
78
+ ofTotal: "% of total",
79
+ sessionsCount: "Sessions",
80
+ projects: "projects",
81
+ estCost: "Est. Cost",
82
+ basedOnPricing: "Based on API pricing",
83
+ tokenByProject: "Token Usage by Project",
84
+ tokenBreakdown: "Token Breakdown",
85
+ tokenBySkill: "Token Usage by Skill",
86
+ turns: "turns",
87
+ dailyUsage: "Daily Token Usage",
88
+ topSessions: "Top Sessions",
89
+ input: "Input",
90
+ output: "Output",
91
+ cacheReadLabel: "Cache Read",
92
+ cacheWriteLabel: "Cache Write",
93
+ msgs: "messages",
94
+ sessionNotFound: "Session not found",
95
+ backToOverview: "Back to Overview",
96
+ session: "Session",
97
+ tokens: "tokens",
98
+ tokenUsagePerTurn: "Token Usage per Turn",
99
+ conversationTurns: "Conversation Turns",
100
+ turn: "#",
101
+ time: "Time",
102
+ message: "Message",
103
+ inputCol: "Input",
104
+ outputCol: "Output",
105
+ cacheRCol: "CacheR",
106
+ cacheWriteCol: "CacheW",
107
+ totalCol: "Total",
108
+ costCol: "Cost",
109
+ totalLabel: "Total",
110
+ sortAsc: "asc",
111
+ sortDesc: "desc",
112
+ turnLabel: "#",
113
+ timeLabel: "Time",
114
+ messageLabel: "Message",
115
+ settings: "Settings",
116
+ modelPricing: "Model Pricing",
117
+ detectedModels: "Detected Models",
118
+ selectPricing: "Select pricing profile",
119
+ customPricing: "Custom",
120
+ inputPrice: "Input Price",
121
+ outputPrice: "Output Price",
122
+ cacheReadPrice: "Cache Read Price",
123
+ perMillionTokens: "per 1M tokens",
124
+ resetToDefaults: "Reset to Defaults",
125
+ noPricing: "No pricing set",
126
+ currency: "Currency",
127
+ sourceFilter: "Data Source",
128
+ allSources: "All",
129
+ claudeCode: "Claude Code",
130
+ codexCli: "Codex CLI",
131
+ },
132
+ } as const;
133
+
134
+ export type Translations = (typeof t)["zh"];
135
+ export type TranslationKey = keyof Translations;
136
+
137
+ export function useTranslations(lang: Lang): Translations {
138
+ return t[lang] as Translations;
139
+ }
140
+
141
+ export const LangContext = createContext<{
142
+ lang: Lang;
143
+ setLang: (l: Lang) => void;
144
+ }>({ lang: "zh", setLang: () => {} });
145
+
146
+ export function useLang() {
147
+ return useContext(LangContext);
148
+ }
package/src/index.css ADDED
@@ -0,0 +1,138 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/geist";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @theme inline {
9
+ --font-heading: var(--font-sans);
10
+ --font-sans: 'Geist Variable', sans-serif;
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --color-foreground: var(--foreground);
41
+ --color-background: var(--background);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ :root {
52
+ --background: oklch(1 0 0);
53
+ --foreground: oklch(0.145 0 0);
54
+ --card: oklch(1 0 0);
55
+ --card-foreground: oklch(0.145 0 0);
56
+ --popover: oklch(1 0 0);
57
+ --popover-foreground: oklch(0.145 0 0);
58
+ --primary: oklch(0.205 0 0);
59
+ --primary-foreground: oklch(0.985 0 0);
60
+ --secondary: oklch(0.97 0 0);
61
+ --secondary-foreground: oklch(0.205 0 0);
62
+ --muted: oklch(0.97 0 0);
63
+ --muted-foreground: oklch(0.556 0 0);
64
+ --accent: oklch(0.97 0 0);
65
+ --accent-foreground: oklch(0.205 0 0);
66
+ --destructive: oklch(0.577 0.245 27.325);
67
+ --border: oklch(0.922 0 0);
68
+ --input: oklch(0.922 0 0);
69
+ --ring: oklch(0.708 0 0);
70
+ --chart-1: oklch(0.87 0 0);
71
+ --chart-2: oklch(0.556 0 0);
72
+ --chart-3: oklch(0.439 0 0);
73
+ --chart-4: oklch(0.371 0 0);
74
+ --chart-5: oklch(0.269 0 0);
75
+ --radius: 0.625rem;
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.145 0 0);
78
+ --sidebar-primary: oklch(0.205 0 0);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.97 0 0);
81
+ --sidebar-accent-foreground: oklch(0.205 0 0);
82
+ --sidebar-border: oklch(0.922 0 0);
83
+ --sidebar-ring: oklch(0.708 0 0);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.145 0 0);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.205 0 0);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.205 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.922 0 0);
94
+ --primary-foreground: oklch(0.205 0 0);
95
+ --secondary: oklch(0.269 0 0);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.269 0 0);
98
+ --muted-foreground: oklch(0.708 0 0);
99
+ --accent: oklch(0.269 0 0);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.556 0 0);
105
+ --chart-1: oklch(0.87 0 0);
106
+ --chart-2: oklch(0.556 0 0);
107
+ --chart-3: oklch(0.439 0 0);
108
+ --chart-4: oklch(0.371 0 0);
109
+ --chart-5: oklch(0.269 0 0);
110
+ --sidebar: oklch(0.205 0 0);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.269 0 0);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.556 0 0);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ html {
128
+ @apply font-sans;
129
+ }
130
+ }
131
+
132
+ @keyframes slide-in {
133
+ from { transform: translateX(100%); }
134
+ to { transform: translateX(0); }
135
+ }
136
+ .animate-slide-in {
137
+ animation: slide-in 0.2s ease-out;
138
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )