mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-20 07:34:27 +01:00
ui-v2: Integrate bossui + chart component (#2622)
This commit is contained in:
21
ui-v2/components.json
Normal file
21
ui-v2/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://localhost:3000/r/registry.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/styles/main.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
6
ui-v2/lib/utils.ts
Normal file
6
ui-v2/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
936
ui-v2/package-lock.json
generated
936
ui-v2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,9 +34,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-router": "^1.120.5",
|
"@tanstack/react-router": "^1.120.5",
|
||||||
"@tanstack/router": "^0.0.1-beta.53",
|
"@tanstack/router": "^0.0.1-beta.53",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.511.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"recharts": "^2.15.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron-forge/cli": "^7.8.1",
|
||||||
@@ -48,6 +51,7 @@
|
|||||||
"@electron-forge/shared-types": "^7.8.1",
|
"@electron-forge/shared-types": "^7.8.1",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -75,11 +79,11 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
"tw-animate-css": "^1.3.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vitest": "^3.1.3"
|
"vitest": "^3.1.3"
|
||||||
@@ -98,4 +102,4 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"forge": "./forge.config.ts"
|
"forge": "./forge.config.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const App: React.FC = (): React.ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="titlebar-drag-region" />
|
<div className="titlebar-drag-region" />
|
||||||
<div className="h-10 border-b-1 w-full" />
|
<div className="h-10 w-full" />
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="">
|
<div className="">
|
||||||
<Suspense fallback={<SuspenseLoader />}>
|
<Suspense fallback={<SuspenseLoader />}>
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
|
||||||
|
|
||||||
interface ChartTileProps {
|
|
||||||
title: string;
|
|
||||||
value: string;
|
|
||||||
trend?: string;
|
|
||||||
data: number[];
|
|
||||||
icon: React.ReactNode;
|
|
||||||
variant?: 'line' | 'bar';
|
|
||||||
date?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChartTile({
|
|
||||||
title,
|
|
||||||
value,
|
|
||||||
trend,
|
|
||||||
data,
|
|
||||||
icon,
|
|
||||||
variant = 'line',
|
|
||||||
date
|
|
||||||
}: ChartTileProps) {
|
|
||||||
const { contentCardStyle } = useTimelineStyles(date);
|
|
||||||
|
|
||||||
// Convert data points to SVG coordinates
|
|
||||||
const createSmoothPath = () => {
|
|
||||||
const points = data.map((value, index) => {
|
|
||||||
const x = (index / (data.length - 1)) * 100;
|
|
||||||
const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100;
|
|
||||||
return [x, y];
|
|
||||||
});
|
|
||||||
|
|
||||||
let path = `M ${points[0][0]},${points[0][1]}`;
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
|
||||||
const current = points[i];
|
|
||||||
const next = points[i + 1];
|
|
||||||
const controlPoint1X = current[0] + (next[0] - current[0]) / 3;
|
|
||||||
const controlPoint2X = current[0] + 2 * (next[0] - current[0]) / 3;
|
|
||||||
path += ` C ${controlPoint1X},${current[1]} ${controlPoint2X},${next[1]} ${next[0]},${next[1]}`;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create bar chart elements
|
|
||||||
const createBars = () => {
|
|
||||||
const maxValue = Math.max(...data.map(Math.abs));
|
|
||||||
const barWidth = 8;
|
|
||||||
const spacing = (100 - (data.length * barWidth)) / (data.length - 1);
|
|
||||||
|
|
||||||
return data.map((value, index) => {
|
|
||||||
const x = index * (barWidth + spacing);
|
|
||||||
const height = Math.abs(value) / maxValue * 50;
|
|
||||||
const y = value > 0 ? 50 - height : 50;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
height,
|
|
||||||
isPositive: value > 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
flex flex-col justify-between
|
|
||||||
w-[320px] h-[380px]
|
|
||||||
${contentCardStyle}
|
|
||||||
rounded-[18px]
|
|
||||||
relative
|
|
||||||
overflow-hidden
|
|
||||||
transition-all duration-200
|
|
||||||
hover:scale-[1.02]
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{/* Header section with icon */}
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="w-6 h-6">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-600 dark:text-white/40 text-sm mb-1">{title}</div>
|
|
||||||
<div className="text-gray-900 dark:text-white text-2xl font-semibold">
|
|
||||||
{value}
|
|
||||||
{trend && <span className="ml-1 text-sm">{trend}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart Container */}
|
|
||||||
<div className="w-full h-[160px]">
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
className="relative z-10"
|
|
||||||
>
|
|
||||||
{variant === 'line' ? (
|
|
||||||
<>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="chart-gradient" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stopColor="#00CAF7" />
|
|
||||||
<stop offset="100%" stopColor="#0B54DE" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<path
|
|
||||||
d={createSmoothPath()}
|
|
||||||
style={{ stroke: 'url(#chart-gradient)' }}
|
|
||||||
className="stroke-[1.5] fill-none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Data points */}
|
|
||||||
{data.map((value, index) => {
|
|
||||||
const x = (index / (data.length - 1)) * 100;
|
|
||||||
const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100;
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
key={index}
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r="2"
|
|
||||||
className="fill-blue-500"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{createBars().map((bar, index) => (
|
|
||||||
<rect
|
|
||||||
key={index}
|
|
||||||
x={bar.x}
|
|
||||||
y={bar.y}
|
|
||||||
width="8"
|
|
||||||
height={bar.height}
|
|
||||||
className={`${bar.isPositive ? 'fill-green-500' : 'fill-red-500'} opacity-80`}
|
|
||||||
rx="4"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
|
||||||
|
|
||||||
interface PieChartSegment {
|
|
||||||
value: number;
|
|
||||||
color: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PieChartTileProps {
|
|
||||||
title: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
segments: PieChartSegment[];
|
|
||||||
date?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PieChartTile({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
segments,
|
|
||||||
date
|
|
||||||
}: PieChartTileProps) {
|
|
||||||
const { contentCardStyle } = useTimelineStyles(date);
|
|
||||||
|
|
||||||
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
||||||
let currentAngle = 0;
|
|
||||||
|
|
||||||
const createPieSegments = () => {
|
|
||||||
return segments.map((segment, index) => {
|
|
||||||
const startAngle = currentAngle;
|
|
||||||
const percentage = segment.value / total;
|
|
||||||
const angle = percentage * 360;
|
|
||||||
currentAngle += angle;
|
|
||||||
|
|
||||||
const startX = Math.cos((startAngle - 90) * Math.PI / 180) * 32 + 50;
|
|
||||||
const startY = Math.sin((startAngle - 90) * Math.PI / 180) * 32 + 50;
|
|
||||||
const endX = Math.cos((currentAngle - 90) * Math.PI / 180) * 32 + 50;
|
|
||||||
const endY = Math.sin((currentAngle - 90) * Math.PI / 180) * 32 + 50;
|
|
||||||
|
|
||||||
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
||||||
|
|
||||||
const d = [
|
|
||||||
`M 50 50`,
|
|
||||||
`L ${startX} ${startY}`,
|
|
||||||
`A 32 32 0 ${largeArcFlag} 1 ${endX} ${endY}`,
|
|
||||||
'Z'
|
|
||||||
].join(' ');
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: d,
|
|
||||||
color: segment.color,
|
|
||||||
label: segment.label,
|
|
||||||
percentage: (percentage * 100).toFixed(1)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const pieSegments = createPieSegments();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
flex flex-col
|
|
||||||
w-[320px] h-[380px]
|
|
||||||
${contentCardStyle}
|
|
||||||
rounded-[18px]
|
|
||||||
relative
|
|
||||||
overflow-hidden
|
|
||||||
transition-all duration-200
|
|
||||||
hover:scale-[1.02]
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="w-6 h-6 mb-4">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-600 dark:text-white/40 text-sm">
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pie Chart */}
|
|
||||||
<div className="flex-1 flex flex-col items-center">
|
|
||||||
<div className="relative w-[150px] h-[150px]">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
className="w-full h-full transform -rotate-90"
|
|
||||||
>
|
|
||||||
{pieSegments.map((segment, index) => (
|
|
||||||
<path
|
|
||||||
key={index}
|
|
||||||
d={segment.path}
|
|
||||||
fill={segment.color}
|
|
||||||
className="transition-all duration-200 hover:opacity-90"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="mt-4 px-4 w-full space-y-2">
|
|
||||||
{pieSegments.map((segment, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
|
||||||
style={{ backgroundColor: segment.color }}
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-white/60">
|
|
||||||
{segment.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{segment.percentage}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
import React, { useRef, useMemo, useEffect } from 'react';
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
import ChartTile from './ChartTile';
|
import ChartTile from './tiles/ChartTile.tsx';
|
||||||
import HighlightTile from './HighlightTile';
|
import HighlightTile from './tiles/HighlightTile.tsx';
|
||||||
import PieChartTile from './PieChartTile';
|
import PieChartTile from './tiles/PieChartTile.tsx';
|
||||||
import ListTile from './ListTile';
|
import ListTile from './tiles/ListTile.tsx';
|
||||||
import ClockTile from './ClockTile';
|
import ClockTile from './tiles/ClockTile.tsx';
|
||||||
import TimelineDots from './TimelineDots';
|
import TimelineDots from './TimelineDots';
|
||||||
import { ChartLineIcon, ChartBarIcon, PieChartIcon, ListIcon, StarIcon, TrendingUpIcon } from './icons';
|
import {
|
||||||
|
ChartLineIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
PieChartIcon,
|
||||||
|
ListIcon,
|
||||||
|
StarIcon,
|
||||||
|
TrendingUpIcon,
|
||||||
|
} from './icons';
|
||||||
|
|
||||||
const generateRandomData = (length: number) => Array.from({ length }, () => Math.floor(Math.random() * 100));
|
const generateRandomData = (length: number) =>
|
||||||
|
Array.from({ length }, () => Math.floor(Math.random() * 100));
|
||||||
|
|
||||||
const generateTileData = (date: Date) => {
|
const generateTileData = (date: Date) => {
|
||||||
const isToday = new Date().toDateString() === date.toDateString();
|
const isToday = new Date().toDateString() === date.toDateString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: [
|
left: [
|
||||||
// Performance metrics
|
// Performance metrics
|
||||||
@@ -24,8 +32,8 @@ const generateTileData = (date: Date) => {
|
|||||||
data: generateRandomData(7),
|
data: generateRandomData(7),
|
||||||
icon: <ChartLineIcon />,
|
icon: <ChartLineIcon />,
|
||||||
variant: 'line' as const,
|
variant: 'line' as const,
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'highlight' as const,
|
type: 'highlight' as const,
|
||||||
@@ -35,8 +43,8 @@ const generateTileData = (date: Date) => {
|
|||||||
icon: <StarIcon />,
|
icon: <StarIcon />,
|
||||||
subtitle: isToday ? 'Personal best today' : 'Keep it up',
|
subtitle: isToday ? 'Personal best today' : 'Keep it up',
|
||||||
date,
|
date,
|
||||||
accentColor: '#FFB800'
|
accentColor: '#FFB800',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'pie' as const,
|
type: 'pie' as const,
|
||||||
@@ -46,10 +54,10 @@ const generateTileData = (date: Date) => {
|
|||||||
segments: [
|
segments: [
|
||||||
{ value: 45, color: '#00CAF7', label: 'Completed' },
|
{ value: 45, color: '#00CAF7', label: 'Completed' },
|
||||||
{ value: 35, color: '#FFB800', label: 'In Progress' },
|
{ value: 35, color: '#FFB800', label: 'In Progress' },
|
||||||
{ value: 20, color: '#FF4444', label: 'Pending' }
|
{ value: 20, color: '#FF4444', label: 'Pending' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Additional metrics
|
// Additional metrics
|
||||||
{
|
{
|
||||||
@@ -61,8 +69,8 @@ const generateTileData = (date: Date) => {
|
|||||||
data: generateRandomData(7),
|
data: generateRandomData(7),
|
||||||
icon: <ChartBarIcon />,
|
icon: <ChartBarIcon />,
|
||||||
variant: 'bar' as const,
|
variant: 'bar' as const,
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'highlight' as const,
|
type: 'highlight' as const,
|
||||||
@@ -72,8 +80,8 @@ const generateTileData = (date: Date) => {
|
|||||||
icon: <StarIcon />,
|
icon: <StarIcon />,
|
||||||
subtitle: 'Based on feedback',
|
subtitle: 'Based on feedback',
|
||||||
date,
|
date,
|
||||||
accentColor: '#4CAF50'
|
accentColor: '#4CAF50',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'list' as const,
|
type: 'list' as const,
|
||||||
@@ -84,10 +92,10 @@ const generateTileData = (date: Date) => {
|
|||||||
{ text: 'Project Alpha', value: '87%', color: '#00CAF7' },
|
{ text: 'Project Alpha', value: '87%', color: '#00CAF7' },
|
||||||
{ text: 'Team Meeting', value: '2:30 PM' },
|
{ text: 'Team Meeting', value: '2:30 PM' },
|
||||||
{ text: 'Review Code', value: '13', color: '#FFB800' },
|
{ text: 'Review Code', value: '13', color: '#FFB800' },
|
||||||
{ text: 'Deploy Update', value: 'Done', color: '#4CAF50' }
|
{ text: 'Deploy Update', value: 'Done', color: '#4CAF50' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// System metrics
|
// System metrics
|
||||||
{
|
{
|
||||||
@@ -99,8 +107,8 @@ const generateTileData = (date: Date) => {
|
|||||||
data: generateRandomData(7),
|
data: generateRandomData(7),
|
||||||
icon: <ChartLineIcon />,
|
icon: <ChartLineIcon />,
|
||||||
variant: 'line' as const,
|
variant: 'line' as const,
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'pie' as const,
|
type: 'pie' as const,
|
||||||
@@ -110,11 +118,11 @@ const generateTileData = (date: Date) => {
|
|||||||
segments: [
|
segments: [
|
||||||
{ value: 60, color: '#4CAF50', label: 'Free' },
|
{ value: 60, color: '#4CAF50', label: 'Free' },
|
||||||
{ value: 25, color: '#FFB800', label: 'Used' },
|
{ value: 25, color: '#FFB800', label: 'Used' },
|
||||||
{ value: 15, color: '#FF4444', label: 'System' }
|
{ value: 15, color: '#FF4444', label: 'System' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
right: [
|
right: [
|
||||||
// Performance metrics
|
// Performance metrics
|
||||||
@@ -127,16 +135,16 @@ const generateTileData = (date: Date) => {
|
|||||||
data: generateRandomData(7),
|
data: generateRandomData(7),
|
||||||
icon: <ChartBarIcon />,
|
icon: <ChartBarIcon />,
|
||||||
variant: 'bar' as const,
|
variant: 'bar' as const,
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Clock tile
|
// Clock tile
|
||||||
{
|
{
|
||||||
type: 'clock' as const,
|
type: 'clock' as const,
|
||||||
props: {
|
props: {
|
||||||
title: 'Current Time',
|
title: 'Current Time',
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'highlight' as const,
|
type: 'highlight' as const,
|
||||||
@@ -146,8 +154,8 @@ const generateTileData = (date: Date) => {
|
|||||||
icon: <TrendingUpIcon />,
|
icon: <TrendingUpIcon />,
|
||||||
subtitle: 'Above target',
|
subtitle: 'Above target',
|
||||||
date,
|
date,
|
||||||
accentColor: '#4CAF50'
|
accentColor: '#4CAF50',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'pie' as const,
|
type: 'pie' as const,
|
||||||
@@ -157,10 +165,10 @@ const generateTileData = (date: Date) => {
|
|||||||
segments: [
|
segments: [
|
||||||
{ value: 55, color: '#4CAF50', label: 'Available' },
|
{ value: 55, color: '#4CAF50', label: 'Available' },
|
||||||
{ value: 30, color: '#FFB800', label: 'In Use' },
|
{ value: 30, color: '#FFB800', label: 'In Use' },
|
||||||
{ value: 15, color: '#FF4444', label: 'Reserved' }
|
{ value: 15, color: '#FF4444', label: 'Reserved' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Updates and notifications
|
// Updates and notifications
|
||||||
{
|
{
|
||||||
@@ -172,10 +180,10 @@ const generateTileData = (date: Date) => {
|
|||||||
{ text: 'System Update', value: 'Complete', color: '#4CAF50' },
|
{ text: 'System Update', value: 'Complete', color: '#4CAF50' },
|
||||||
{ text: 'New Features', value: '3', color: '#00CAF7' },
|
{ text: 'New Features', value: '3', color: '#00CAF7' },
|
||||||
{ text: 'Bug Fixes', value: '7', color: '#FFB800' },
|
{ text: 'Bug Fixes', value: '7', color: '#FFB800' },
|
||||||
{ text: 'Performance', value: '+15%', color: '#4CAF50' }
|
{ text: 'Performance', value: '+15%', color: '#4CAF50' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Additional metrics
|
// Additional metrics
|
||||||
{
|
{
|
||||||
@@ -187,8 +195,8 @@ const generateTileData = (date: Date) => {
|
|||||||
data: generateRandomData(7),
|
data: generateRandomData(7),
|
||||||
icon: <ChartLineIcon />,
|
icon: <ChartLineIcon />,
|
||||||
variant: 'line' as const,
|
variant: 'line' as const,
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'highlight' as const,
|
type: 'highlight' as const,
|
||||||
@@ -198,8 +206,8 @@ const generateTileData = (date: Date) => {
|
|||||||
icon: <TrendingUpIcon />,
|
icon: <TrendingUpIcon />,
|
||||||
subtitle: 'Last 24 hours',
|
subtitle: 'Last 24 hours',
|
||||||
date,
|
date,
|
||||||
accentColor: '#00CAF7'
|
accentColor: '#00CAF7',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// System health
|
// System health
|
||||||
{
|
{
|
||||||
@@ -210,10 +218,10 @@ const generateTileData = (date: Date) => {
|
|||||||
segments: [
|
segments: [
|
||||||
{ value: 75, color: '#4CAF50', label: 'Healthy' },
|
{ value: 75, color: '#4CAF50', label: 'Healthy' },
|
||||||
{ value: 20, color: '#FFB800', label: 'Warning' },
|
{ value: 20, color: '#FFB800', label: 'Warning' },
|
||||||
{ value: 5, color: '#FF4444', label: 'Critical' }
|
{ value: 5, color: '#FF4444', label: 'Critical' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'list' as const,
|
type: 'list' as const,
|
||||||
@@ -224,12 +232,12 @@ const generateTileData = (date: Date) => {
|
|||||||
{ text: 'Main API', value: 'Online', color: '#4CAF50' },
|
{ text: 'Main API', value: 'Online', color: '#4CAF50' },
|
||||||
{ text: 'Database', value: '98%', color: '#00CAF7' },
|
{ text: 'Database', value: '98%', color: '#00CAF7' },
|
||||||
{ text: 'Cache', value: 'Synced', color: '#4CAF50' },
|
{ text: 'Cache', value: 'Synced', color: '#4CAF50' },
|
||||||
{ text: 'CDN', value: 'Active', color: '#4CAF50' }
|
{ text: 'CDN', value: 'Active', color: '#4CAF50' },
|
||||||
],
|
],
|
||||||
date
|
date,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,7 +248,7 @@ export default function Timeline() {
|
|||||||
const sections = useMemo(() => {
|
const sections = useMemo(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
for (let i = 0; i <= 29; i++) {
|
for (let i = 0; i <= 29; i++) {
|
||||||
const date = new Date(today);
|
const date = new Date(today);
|
||||||
date.setDate(today.getDate() - i);
|
date.setDate(today.getDate() - i);
|
||||||
@@ -251,10 +259,10 @@ export default function Timeline() {
|
|||||||
date,
|
date,
|
||||||
isToday: i === 0,
|
isToday: i === 0,
|
||||||
leftTiles: tileData.left,
|
leftTiles: tileData.left,
|
||||||
rightTiles: tileData.right
|
rightTiles: tileData.right,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -269,7 +277,7 @@ export default function Timeline() {
|
|||||||
|
|
||||||
sectionElement.scrollTo({
|
sectionElement.scrollTo({
|
||||||
left: scrollToX,
|
left: scrollToX,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -287,14 +295,14 @@ export default function Timeline() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
rootMargin: '0px'
|
rootMargin: '0px',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add resize handler
|
// Add resize handler
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
// Find the currently visible section
|
// Find the currently visible section
|
||||||
const visibleSection = sectionRefs.current.find(section => {
|
const visibleSection = sectionRefs.current.find((section) => {
|
||||||
if (!section) return false;
|
if (!section) return false;
|
||||||
const rect = section.getBoundingClientRect();
|
const rect = section.getBoundingClientRect();
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
@@ -347,23 +355,25 @@ export default function Timeline() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="h-screen overflow-y-scroll overflow-x-hidden snap-y snap-mandatory relative scrollbar-hide"
|
className="h-screen overflow-y-scroll overflow-x-hidden snap-y snap-mandatory relative scrollbar-hide"
|
||||||
>
|
>
|
||||||
{sections.map((section, index) => (
|
{sections.map((section, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
ref={el => sectionRefs.current[index] = el}
|
ref={(el) => (sectionRefs.current[index] = el)}
|
||||||
className="h-screen relative snap-center snap-always overflow-x-scroll snap-x snap-mandatory scrollbar-hide"
|
className="h-screen relative snap-center snap-always overflow-y-hidden overflow-x-scroll snap-x snap-mandatory scrollbar-hide"
|
||||||
>
|
>
|
||||||
<div className="relative min-w-[calc(200vw+100px)] h-full flex items-center">
|
<div className="relative min-w-[calc(200vw+100px)] h-full flex items-center">
|
||||||
{/* Main flex container */}
|
{/* Main flex container */}
|
||||||
<div className="w-full h-full flex">
|
<div className="w-full h-full flex">
|
||||||
|
|
||||||
{/* Left Grid */}
|
{/* Left Grid */}
|
||||||
<div className="w-screen p-4 mt-6 overflow-hidden">
|
<div className="w-screen p-4 mt-6 overflow-hidden">
|
||||||
<div className="ml-auto mr-0 flex flex-wrap gap-4 content-start justify-end" style={{ width: 'min(720px, 90%)' }}>
|
<div
|
||||||
|
className="ml-auto mr-0 flex flex-wrap gap-4 content-start justify-end"
|
||||||
|
style={{ width: 'min(720px, 90%)' }}
|
||||||
|
>
|
||||||
{section.leftTiles.map((tile, i) => (
|
{section.leftTiles.map((tile, i) => (
|
||||||
<div key={i} className="w-[calc(50%-8px)]">
|
<div key={i} className="w-[calc(50%-8px)]">
|
||||||
{renderTile(tile, i)}
|
{renderTile(tile, i)}
|
||||||
@@ -375,28 +385,45 @@ export default function Timeline() {
|
|||||||
{/* Center Timeline */}
|
{/* Center Timeline */}
|
||||||
<div className="w-100px relative flex flex-col items-center h-screen">
|
<div className="w-100px relative flex flex-col items-center h-screen">
|
||||||
{/* Upper Timeline Dots */}
|
{/* Upper Timeline Dots */}
|
||||||
<TimelineDots height="calc(50vh - 96px)" isUpper={true} isCurrentDay={section.isToday} />
|
<TimelineDots
|
||||||
|
height="calc(50vh - 96px)"
|
||||||
|
isUpper={true}
|
||||||
|
isCurrentDay={section.isToday}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Date Display */}
|
{/* Date Display */}
|
||||||
<div className="bg-white p-4 rounded z-[3] flex flex-col items-center transition-opacity">
|
<div className="bg-white p-4 rounded z-[3] flex flex-col items-center transition-opacity">
|
||||||
<div className={`font-['Cash_Sans'] text-3xl font-light ${section.isToday ? 'opacity-100' : 'opacity-20'}`}>
|
<div
|
||||||
|
className={`font-['Cash_Sans'] text-3xl font-light ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||||
|
>
|
||||||
{section.date.toLocaleString('default', { month: 'short' })}
|
{section.date.toLocaleString('default', { month: 'short' })}
|
||||||
</div>
|
</div>
|
||||||
<div className={`font-['Cash_Sans'] text-[64px] font-light leading-none ${section.isToday ? 'opacity-100' : 'opacity-20'}`}>
|
<div
|
||||||
|
className={`font-['Cash_Sans'] text-[64px] font-light leading-none ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||||
|
>
|
||||||
{section.date.getDate()}
|
{section.date.getDate()}
|
||||||
</div>
|
</div>
|
||||||
<div className={`font-['Cash_Sans'] text-sm font-light mt-1 ${section.isToday ? 'opacity-100' : 'opacity-20'}`}>
|
<div
|
||||||
|
className={`font-['Cash_Sans'] text-sm font-light mt-1 ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||||
|
>
|
||||||
{section.date.toLocaleString('default', { weekday: 'long' })}
|
{section.date.toLocaleString('default', { weekday: 'long' })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lower Timeline Dots */}
|
{/* Lower Timeline Dots */}
|
||||||
<TimelineDots height="calc(50vh - 96px)" isUpper={false} isCurrentDay={section.isToday} />
|
<TimelineDots
|
||||||
|
height="calc(50vh - 96px)"
|
||||||
|
isUpper={false}
|
||||||
|
isCurrentDay={section.isToday}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Grid */}
|
{/* Right Grid */}
|
||||||
<div className="w-screen p-4 mt-6 overflow-hidden">
|
<div className="w-screen p-4 mt-6 overflow-hidden">
|
||||||
<div className="flex flex-wrap gap-4 content-start" style={{ width: 'min(720px, 90%)' }}>
|
<div
|
||||||
|
className="flex flex-wrap gap-4 content-start"
|
||||||
|
style={{ width: 'min(720px, 90%)' }}
|
||||||
|
>
|
||||||
{section.rightTiles.map((tile, i) => (
|
{section.rightTiles.map((tile, i) => (
|
||||||
<div key={i} className="w-[calc(50%-8px)]">
|
<div key={i} className="w-[calc(50%-8px)]">
|
||||||
{renderTile(tile, i)}
|
{renderTile(tile, i)}
|
||||||
@@ -410,4 +437,4 @@ export default function Timeline() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
100
ui-v2/src/components/tiles/ChartTile.tsx
Normal file
100
ui-v2/src/components/tiles/ChartTile.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||||
|
import { ChartConfig, ChartContainer } from "@/components/ui/chart.tsx";
|
||||||
|
import { BarChart, Bar, LineChart, Line, ResponsiveContainer, Tooltip } from 'recharts';
|
||||||
|
|
||||||
|
interface ChartTileProps {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
trend?: string;
|
||||||
|
data: number[];
|
||||||
|
icon: React.ReactNode;
|
||||||
|
variant?: 'line' | 'bar';
|
||||||
|
date?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChartTile({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
trend,
|
||||||
|
data,
|
||||||
|
icon,
|
||||||
|
variant = 'line',
|
||||||
|
date
|
||||||
|
}: ChartTileProps) {
|
||||||
|
const { contentCardStyle } = useTimelineStyles(date);
|
||||||
|
|
||||||
|
// Convert the data array to the format expected by recharts
|
||||||
|
const chartData = data.map((value, index) => ({
|
||||||
|
value,
|
||||||
|
index: `Point ${index + 1}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Chart configuration
|
||||||
|
const chartConfig = {
|
||||||
|
value: {
|
||||||
|
label: title,
|
||||||
|
theme: {
|
||||||
|
light: variant === 'line' ? '#0B54DE' : '#4CAF50',
|
||||||
|
dark: variant === 'line' ? '#00CAF7' : '#4CAF50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex flex-col justify-between
|
||||||
|
w-[320px] h-[380px]
|
||||||
|
${contentCardStyle}
|
||||||
|
rounded-[18px]
|
||||||
|
relative
|
||||||
|
overflow-hidden
|
||||||
|
transition-all duration-200
|
||||||
|
hover:scale-[1.02]
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Header section with icon */}
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="w-6 h-6">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-600 dark:text-white/40 text-sm mb-1">{title}</div>
|
||||||
|
<div className="text-gray-900 dark:text-white text-2xl font-semibold">
|
||||||
|
{value}
|
||||||
|
{trend && <span className="ml-1 text-sm">{trend}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart Container */}
|
||||||
|
<div className="w-full h-[160px] px-4">
|
||||||
|
<ChartContainer config={chartConfig}>
|
||||||
|
{variant === 'line' ? (
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="var(--color-value)"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: 'var(--color-value)', r: 4 }}
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
</LineChart>
|
||||||
|
) : (
|
||||||
|
<BarChart data={chartData}>
|
||||||
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="var(--color-value)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
</BarChart>
|
||||||
|
)}
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||||
import waveBg from '../assets/backgrounds/wave-bg.png';
|
import waveBg from '../../assets/backgrounds/wave-bg.png';
|
||||||
|
|
||||||
interface ClockCardProps {
|
interface ClockCardProps {
|
||||||
date?: Date;
|
date?: Date;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||||
|
|
||||||
interface HighlightTileProps {
|
interface HighlightTileProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||||
|
|
||||||
interface ListItem {
|
interface ListItem {
|
||||||
text: string;
|
text: string;
|
||||||
125
ui-v2/src/components/tiles/PieChartTile.tsx
Normal file
125
ui-v2/src/components/tiles/PieChartTile.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTimelineStyles } from '../../hooks/useTimelineStyles';
|
||||||
|
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||||
|
import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts';
|
||||||
|
|
||||||
|
interface PieChartSegment {
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PieChartTileProps {
|
||||||
|
title: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
segments: PieChartSegment[];
|
||||||
|
date?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PieChartTile({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
segments,
|
||||||
|
date
|
||||||
|
}: PieChartTileProps) {
|
||||||
|
const { contentCardStyle } = useTimelineStyles(date);
|
||||||
|
|
||||||
|
// Convert segments to the format expected by recharts
|
||||||
|
const chartData = segments.map(segment => ({
|
||||||
|
name: segment.label,
|
||||||
|
value: segment.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create chart configuration with theme colors
|
||||||
|
const chartConfig = segments.reduce((config, segment) => {
|
||||||
|
config[segment.label] = {
|
||||||
|
label: segment.label,
|
||||||
|
color: segment.color
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
}, {} as ChartConfig);
|
||||||
|
|
||||||
|
// Custom tooltip formatter
|
||||||
|
const tooltipFormatter = (value: number, name: string) => {
|
||||||
|
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
||||||
|
const percentage = ((value / total) * 100).toFixed(1);
|
||||||
|
return [`${percentage}%`, name];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex flex-col
|
||||||
|
w-[320px] h-[380px]
|
||||||
|
${contentCardStyle}
|
||||||
|
rounded-[18px]
|
||||||
|
relative
|
||||||
|
overflow-hidden
|
||||||
|
transition-all duration-200
|
||||||
|
hover:scale-[1.02]
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="w-6 h-6 mb-4">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600 dark:text-white/40 text-sm">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pie Chart */}
|
||||||
|
<div className="flex-1 flex flex-col items-center">
|
||||||
|
<div className="w-full h-[200px]">
|
||||||
|
<ChartContainer config={chartConfig}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={0}
|
||||||
|
outerRadius={70}
|
||||||
|
paddingAngle={2}
|
||||||
|
>
|
||||||
|
{segments.map((segment, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={segment.color}
|
||||||
|
className="transition-all duration-200 hover:opacity-90"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip formatter={tooltipFormatter} />
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="mt-2 px-4 w-full space-y-2">
|
||||||
|
{segments.map((segment, index) => {
|
||||||
|
const percentage = ((segment.value / segments.reduce((sum, s) => sum + s.value, 0)) * 100).toFixed(1);
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full mr-2"
|
||||||
|
style={{ backgroundColor: segment.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-white/60">
|
||||||
|
{segment.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{percentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
353
ui-v2/src/components/ui/chart.tsx
Normal file
353
ui-v2/src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
icon?: React.ComponentType;
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartContainer({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig;
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"];
|
||||||
|
}) {
|
||||||
|
const uniqueId = React.useId();
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-slot="chart"
|
||||||
|
data-chart={chartId}
|
||||||
|
className={cn(
|
||||||
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([, config]) => config.theme || config.color
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color;
|
||||||
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
|
function ChartTooltipContent({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean;
|
||||||
|
hideIndicator?: boolean;
|
||||||
|
indicator?: "line" | "dot" | "dashed";
|
||||||
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
}) {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload;
|
||||||
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label;
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"border-border/50 bg-background-default grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-text-muted flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
|
indicator === "dot" && "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-text-muted">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="text-text-default font-mono tabular-nums">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
|
function ChartLegendContent({
|
||||||
|
className,
|
||||||
|
hideIcon = false,
|
||||||
|
payload,
|
||||||
|
verticalAlign = "bottom",
|
||||||
|
nameKey,
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean;
|
||||||
|
nameKey?: string;
|
||||||
|
}) {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-text-muted flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
};
|
||||||
4
ui-v2/src/components/ui/readme.md
Normal file
4
ui-v2/src/components/ui/readme.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Files in this directory are automatically generated/pulled in from the shadcn registry.
|
||||||
|
|
||||||
|
Add new components like this:
|
||||||
|
```npx shadcn@canary add http://localhost:3000/r/chart.json```
|
||||||
20
ui-v2/src/lib/utils.ts
Normal file
20
ui-v2/src/lib/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copyToClipboard = (text: string) => {
|
||||||
|
if (window === undefined) return;
|
||||||
|
window.navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getComponentName(name: string) {
|
||||||
|
// convert kebab-case to title case
|
||||||
|
return name.replace(/-/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomIndex(array: any[]) {
|
||||||
|
return Math.floor(Math.random() * array.length);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
|
|
||||||
import { router } from './routeTree';
|
import { router } from './routeTree';
|
||||||
|
|
||||||
import './index.css';
|
import './styles/main.css';
|
||||||
|
|
||||||
// Initialize the router
|
// Initialize the router
|
||||||
await router.load();
|
await router.load();
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
@import url('tailwindcss');
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@tailwind base;
|
@custom-variant dark (&:is(.dark *));
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
@@ -133,9 +132,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Cash Sans';
|
font-family: 'Cash Sans';
|
||||||
src:
|
src:
|
||||||
@@ -177,3 +173,119 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]): string {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
@@ -20,7 +24,6 @@
|
|||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"baseUrl": ".",
|
|
||||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||||
},
|
},
|
||||||
"include": ["src", "src/test/types.d.ts"],
|
"include": ["src", "src/test/types.d.ts"],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
'@shared': path.resolve(__dirname, './shared'),
|
'@shared': path.resolve(__dirname, './shared'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
'@shared': path.resolve(__dirname, './shared'),
|
'@shared': path.resolve(__dirname, './shared'),
|
||||||
'@platform': path.resolve(__dirname, './src/services/platform/electron'),
|
'@platform': path.resolve(__dirname, './src/services/platform/electron'),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user