mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-20 15:44:25 +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": {
|
||||
"@tanstack/react-router": "^1.120.5",
|
||||
"@tanstack/router": "^0.0.1-beta.53",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
@@ -48,6 +51,7 @@
|
||||
"@electron-forge/shared-types": "^7.8.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -75,11 +79,11 @@
|
||||
"prettier": "^3.5.3",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.3"
|
||||
|
||||
@@ -8,7 +8,7 @@ const App: React.FC = (): React.ReactElement => {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="titlebar-drag-region" />
|
||||
<div className="h-10 border-b-1 w-full" />
|
||||
<div className="h-10 w-full" />
|
||||
<div className="">
|
||||
<div className="">
|
||||
<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,13 +1,21 @@
|
||||
import React, { useRef, useMemo, useEffect } from 'react';
|
||||
import ChartTile from './ChartTile';
|
||||
import HighlightTile from './HighlightTile';
|
||||
import PieChartTile from './PieChartTile';
|
||||
import ListTile from './ListTile';
|
||||
import ClockTile from './ClockTile';
|
||||
import ChartTile from './tiles/ChartTile.tsx';
|
||||
import HighlightTile from './tiles/HighlightTile.tsx';
|
||||
import PieChartTile from './tiles/PieChartTile.tsx';
|
||||
import ListTile from './tiles/ListTile.tsx';
|
||||
import ClockTile from './tiles/ClockTile.tsx';
|
||||
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 isToday = new Date().toDateString() === date.toDateString();
|
||||
@@ -24,8 +32,8 @@ const generateTileData = (date: Date) => {
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
@@ -35,8 +43,8 @@ const generateTileData = (date: Date) => {
|
||||
icon: <StarIcon />,
|
||||
subtitle: isToday ? 'Personal best today' : 'Keep it up',
|
||||
date,
|
||||
accentColor: '#FFB800'
|
||||
}
|
||||
accentColor: '#FFB800',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
@@ -46,10 +54,10 @@ const generateTileData = (date: Date) => {
|
||||
segments: [
|
||||
{ value: 45, color: '#00CAF7', label: 'Completed' },
|
||||
{ value: 35, color: '#FFB800', label: 'In Progress' },
|
||||
{ value: 20, color: '#FF4444', label: 'Pending' }
|
||||
{ value: 20, color: '#FF4444', label: 'Pending' },
|
||||
],
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Additional metrics
|
||||
{
|
||||
@@ -61,8 +69,8 @@ const generateTileData = (date: Date) => {
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartBarIcon />,
|
||||
variant: 'bar' as const,
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
@@ -72,8 +80,8 @@ const generateTileData = (date: Date) => {
|
||||
icon: <StarIcon />,
|
||||
subtitle: 'Based on feedback',
|
||||
date,
|
||||
accentColor: '#4CAF50'
|
||||
}
|
||||
accentColor: '#4CAF50',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list' as const,
|
||||
@@ -84,10 +92,10 @@ const generateTileData = (date: Date) => {
|
||||
{ text: 'Project Alpha', value: '87%', color: '#00CAF7' },
|
||||
{ text: 'Team Meeting', value: '2:30 PM' },
|
||||
{ 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
|
||||
{
|
||||
@@ -99,8 +107,8 @@ const generateTileData = (date: Date) => {
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
@@ -110,11 +118,11 @@ const generateTileData = (date: Date) => {
|
||||
segments: [
|
||||
{ value: 60, color: '#4CAF50', label: 'Free' },
|
||||
{ value: 25, color: '#FFB800', label: 'Used' },
|
||||
{ value: 15, color: '#FF4444', label: 'System' }
|
||||
{ value: 15, color: '#FF4444', label: 'System' },
|
||||
],
|
||||
date
|
||||
}
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
],
|
||||
right: [
|
||||
// Performance metrics
|
||||
@@ -127,16 +135,16 @@ const generateTileData = (date: Date) => {
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartBarIcon />,
|
||||
variant: 'bar' as const,
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Clock tile
|
||||
{
|
||||
type: 'clock' as const,
|
||||
props: {
|
||||
title: 'Current Time',
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
@@ -146,8 +154,8 @@ const generateTileData = (date: Date) => {
|
||||
icon: <TrendingUpIcon />,
|
||||
subtitle: 'Above target',
|
||||
date,
|
||||
accentColor: '#4CAF50'
|
||||
}
|
||||
accentColor: '#4CAF50',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'pie' as const,
|
||||
@@ -157,10 +165,10 @@ const generateTileData = (date: Date) => {
|
||||
segments: [
|
||||
{ value: 55, color: '#4CAF50', label: 'Available' },
|
||||
{ value: 30, color: '#FFB800', label: 'In Use' },
|
||||
{ value: 15, color: '#FF4444', label: 'Reserved' }
|
||||
{ value: 15, color: '#FF4444', label: 'Reserved' },
|
||||
],
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Updates and notifications
|
||||
{
|
||||
@@ -172,10 +180,10 @@ const generateTileData = (date: Date) => {
|
||||
{ text: 'System Update', value: 'Complete', color: '#4CAF50' },
|
||||
{ text: 'New Features', value: '3', color: '#00CAF7' },
|
||||
{ text: 'Bug Fixes', value: '7', color: '#FFB800' },
|
||||
{ text: 'Performance', value: '+15%', color: '#4CAF50' }
|
||||
{ text: 'Performance', value: '+15%', color: '#4CAF50' },
|
||||
],
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
// Additional metrics
|
||||
{
|
||||
@@ -187,8 +195,8 @@ const generateTileData = (date: Date) => {
|
||||
data: generateRandomData(7),
|
||||
icon: <ChartLineIcon />,
|
||||
variant: 'line' as const,
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'highlight' as const,
|
||||
@@ -198,8 +206,8 @@ const generateTileData = (date: Date) => {
|
||||
icon: <TrendingUpIcon />,
|
||||
subtitle: 'Last 24 hours',
|
||||
date,
|
||||
accentColor: '#00CAF7'
|
||||
}
|
||||
accentColor: '#00CAF7',
|
||||
},
|
||||
},
|
||||
// System health
|
||||
{
|
||||
@@ -210,10 +218,10 @@ const generateTileData = (date: Date) => {
|
||||
segments: [
|
||||
{ value: 75, color: '#4CAF50', label: 'Healthy' },
|
||||
{ value: 20, color: '#FFB800', label: 'Warning' },
|
||||
{ value: 5, color: '#FF4444', label: 'Critical' }
|
||||
{ value: 5, color: '#FF4444', label: 'Critical' },
|
||||
],
|
||||
date
|
||||
}
|
||||
date,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list' as const,
|
||||
@@ -224,12 +232,12 @@ const generateTileData = (date: Date) => {
|
||||
{ text: 'Main API', value: 'Online', color: '#4CAF50' },
|
||||
{ text: 'Database', value: '98%', color: '#00CAF7' },
|
||||
{ text: 'Cache', value: 'Synced', color: '#4CAF50' },
|
||||
{ text: 'CDN', value: 'Active', color: '#4CAF50' }
|
||||
{ text: 'CDN', value: 'Active', color: '#4CAF50' },
|
||||
],
|
||||
date,
|
||||
},
|
||||
},
|
||||
],
|
||||
date
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -251,7 +259,7 @@ export default function Timeline() {
|
||||
date,
|
||||
isToday: i === 0,
|
||||
leftTiles: tileData.left,
|
||||
rightTiles: tileData.right
|
||||
rightTiles: tileData.right,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,7 +277,7 @@ export default function Timeline() {
|
||||
|
||||
sectionElement.scrollTo({
|
||||
left: scrollToX,
|
||||
behavior: 'smooth'
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -287,14 +295,14 @@ export default function Timeline() {
|
||||
},
|
||||
{
|
||||
threshold: 0.5,
|
||||
rootMargin: '0px'
|
||||
rootMargin: '0px',
|
||||
}
|
||||
);
|
||||
|
||||
// Add resize handler
|
||||
const handleResize = () => {
|
||||
// Find the currently visible section
|
||||
const visibleSection = sectionRefs.current.find(section => {
|
||||
const visibleSection = sectionRefs.current.find((section) => {
|
||||
if (!section) return false;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
@@ -354,16 +362,18 @@ export default function Timeline() {
|
||||
{sections.map((section, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={el => sectionRefs.current[index] = el}
|
||||
className="h-screen relative snap-center snap-always overflow-x-scroll snap-x snap-mandatory scrollbar-hide"
|
||||
ref={(el) => (sectionRefs.current[index] = el)}
|
||||
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">
|
||||
{/* Main flex container */}
|
||||
<div className="w-full h-full flex">
|
||||
|
||||
{/* Left Grid */}
|
||||
<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) => (
|
||||
<div key={i} className="w-[calc(50%-8px)]">
|
||||
{renderTile(tile, i)}
|
||||
@@ -375,28 +385,45 @@ export default function Timeline() {
|
||||
{/* Center Timeline */}
|
||||
<div className="w-100px relative flex flex-col items-center h-screen">
|
||||
{/* 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 */}
|
||||
<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' })}
|
||||
</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()}
|
||||
</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' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lower Timeline Dots */}
|
||||
<TimelineDots height="calc(50vh - 96px)" isUpper={false} isCurrentDay={section.isToday} />
|
||||
<TimelineDots
|
||||
height="calc(50vh - 96px)"
|
||||
isUpper={false}
|
||||
isCurrentDay={section.isToday}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Grid */}
|
||||
<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) => (
|
||||
<div key={i} className="w-[calc(50%-8px)]">
|
||||
{renderTile(tile, i)}
|
||||
|
||||
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 { useTimelineStyles } from '../hooks/useTimelineStyles';
|
||||
import waveBg from '../assets/backgrounds/wave-bg.png';
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
import waveBg from '../../assets/backgrounds/wave-bg.png';
|
||||
|
||||
interface ClockCardProps {
|
||||
date?: Date;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
interface HighlightTileProps {
|
||||
title: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTimelineStyles } from '../hooks/useTimelineStyles';
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
|
||||
|
||||
interface ListItem {
|
||||
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 './index.css';
|
||||
import './styles/main.css';
|
||||
|
||||
// Initialize the router
|
||||
await router.load();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@import url('tailwindcss');
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@@ -133,9 +132,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cash Sans';
|
||||
src:
|
||||
@@ -177,3 +173,119 @@
|
||||
left: 0;
|
||||
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": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
@@ -20,7 +24,6 @@
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src", "src/test/types.d.ts"],
|
||||
|
||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, './shared'),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, './shared'),
|
||||
'@platform': path.resolve(__dirname, './src/services/platform/electron'),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user