mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: initialize monorepo with package.json for CCW workflow platform
This commit is contained in:
131
ccw/frontend/src/components/charts/ActivityLineChart.tsx
Normal file
131
ccw/frontend/src/components/charts/ActivityLineChart.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
// ========================================
|
||||
// ActivityLineChart Component
|
||||
// ========================================
|
||||
// Recharts line chart visualizing activity timeline
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import type { ActivityTimelineData } from '@/hooks/useActivityTimeline';
|
||||
import { getChartColors } from '@/lib/chartTheme';
|
||||
|
||||
export interface ActivityLineChartProps {
|
||||
/** Activity timeline data */
|
||||
data: ActivityTimelineData[];
|
||||
/** Optional CSS class name */
|
||||
className?: string;
|
||||
/** Chart height (default: 300) */
|
||||
height?: number;
|
||||
/** Optional label for the chart */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom tooltip component for the line chart
|
||||
*/
|
||||
function CustomTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="rounded bg-card p-3 shadow-md border border-border">
|
||||
<p className="text-sm font-medium text-foreground mb-2">{label}</p>
|
||||
{payload.map((item: any, index: number) => (
|
||||
<p key={index} className="text-sm" style={{ color: item.color }}>
|
||||
{item.name}: {item.value}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for X-axis display (MM/DD)
|
||||
*/
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityLineChart - Visualizes sessions and tasks over time
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, isLoading } = useActivityTimeline();
|
||||
* return <ActivityLineChart data={data} />;
|
||||
* ```
|
||||
*/
|
||||
export function ActivityLineChart({
|
||||
data,
|
||||
className = '',
|
||||
height = 300,
|
||||
title,
|
||||
}: ActivityLineChartProps) {
|
||||
const colors = useMemo(() => getChartColors(), []);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
displayDate: formatDate(item.date),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full ${className}`}
|
||||
role="img"
|
||||
aria-label="Activity timeline line chart showing sessions and tasks over time"
|
||||
>
|
||||
{title && <h3 className="text-lg font-semibold text-foreground mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
|
||||
accessibilityLayer
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={colors.muted} />
|
||||
<XAxis
|
||||
dataKey="displayDate"
|
||||
stroke={colors.muted}
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
<YAxis stroke={colors.muted} style={{ fontSize: '12px' }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '14px' }}
|
||||
iconType="line"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="sessions"
|
||||
stroke={colors.primary}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: colors.primary, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
name="Sessions"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="tasks"
|
||||
stroke={colors.success}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: colors.success, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
name="Tasks"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivityLineChart;
|
||||
111
ccw/frontend/src/components/charts/ChartSkeleton.tsx
Normal file
111
ccw/frontend/src/components/charts/ChartSkeleton.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// ========================================
|
||||
// ChartSkeleton Component
|
||||
// ========================================
|
||||
// Loading skeleton for chart components
|
||||
|
||||
export interface ChartSkeletonProps {
|
||||
/** Skeleton type: pie, line, or bar */
|
||||
type?: 'pie' | 'line' | 'bar';
|
||||
/** Height in pixels (default: 300) */
|
||||
height?: number;
|
||||
/** Optional CSS class name */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ChartSkeleton - Animated loading skeleton for chart components
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, isLoading } = useWorkflowStatusCounts();
|
||||
*
|
||||
* if (isLoading) return <ChartSkeleton type="pie" />;
|
||||
* return <WorkflowStatusPieChart data={data} />;
|
||||
* ```
|
||||
*/
|
||||
export function ChartSkeleton({
|
||||
type = 'bar',
|
||||
height = 300,
|
||||
className = '',
|
||||
}: ChartSkeletonProps) {
|
||||
return (
|
||||
<div className={`w-full animate-pulse ${className}`} style={{ height }}>
|
||||
{type === 'pie' && <PieSkeleton height={height} />}
|
||||
{type === 'line' && <LineSkeleton height={height} />}
|
||||
{type === 'bar' && <BarSkeleton height={height} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pie chart skeleton
|
||||
*/
|
||||
function PieSkeleton({ height }: { height: number }) {
|
||||
const radius = Math.min(height * 0.3, 80);
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-4">
|
||||
<div
|
||||
className="rounded-full bg-muted"
|
||||
style={{ width: radius * 2, height: radius * 2 }}
|
||||
/>
|
||||
<div className="flex gap-4 mt-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-muted" />
|
||||
<div className="w-12 h-3 rounded bg-muted" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Line chart skeleton
|
||||
*/
|
||||
function LineSkeleton({ height: _height }: { height: number }) {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="flex-1 flex items-end gap-2">
|
||||
{[40, 65, 45, 80, 55, 70, 60].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-muted rounded-t"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
|
||||
<div key={i} className="w-8 h-3 rounded bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar chart skeleton
|
||||
*/
|
||||
function BarSkeleton({ height: _height }: { height: number }) {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="flex-1 flex items-end gap-3">
|
||||
{[60, 85, 45, 70, 55, 30].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-muted rounded-t"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="w-10 h-3 rounded bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChartSkeleton;
|
||||
79
ccw/frontend/src/components/charts/Sparkline.tsx
Normal file
79
ccw/frontend/src/components/charts/Sparkline.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
// ========================================
|
||||
// Sparkline Component
|
||||
// ========================================
|
||||
// Mini line chart for trend visualization in StatCards
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { LineChart, Line, ResponsiveContainer } from 'recharts';
|
||||
import { getChartColors } from '@/lib/chartTheme';
|
||||
|
||||
export interface SparklineProps {
|
||||
/** Array of numeric values for the sparkline */
|
||||
data: number[];
|
||||
/** Optional CSS class name */
|
||||
className?: string;
|
||||
/** Chart height in pixels (default: 50) */
|
||||
height?: number;
|
||||
/** Line color (default: primary theme color) */
|
||||
color?: string;
|
||||
/** Line width (default: 2) */
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sparkline - Minimal line chart for at-a-glance trend visualization
|
||||
*
|
||||
* Displays a simple line chart with no axes or labels, optimized for
|
||||
* showing trends in constrained spaces like StatCards.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Show last 7 days of activity
|
||||
* <Sparkline data={[12, 19, 3, 5, 2, 3, 7]} height={40} />
|
||||
* ```
|
||||
*/
|
||||
export function Sparkline({
|
||||
data,
|
||||
className = '',
|
||||
height = 50,
|
||||
color,
|
||||
strokeWidth = 2,
|
||||
}: SparklineProps) {
|
||||
const colors = useMemo(() => getChartColors(), []);
|
||||
const lineColor = color || colors.primary;
|
||||
|
||||
// Transform data into Recharts format
|
||||
const chartData = useMemo(() => {
|
||||
return data.map((value, index) => ({
|
||||
index,
|
||||
value,
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
// Don't render if no data
|
||||
if (!data || data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{ top: 2, right: 2, bottom: 2, left: 2 }}
|
||||
>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={lineColor}
|
||||
strokeWidth={strokeWidth}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sparkline;
|
||||
118
ccw/frontend/src/components/charts/TaskTypeBarChart.tsx
Normal file
118
ccw/frontend/src/components/charts/TaskTypeBarChart.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
// ========================================
|
||||
// TaskTypeBarChart Component
|
||||
// ========================================
|
||||
// Recharts bar chart visualizing task type breakdown
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
import type { TaskTypeCount } from '@/hooks/useTaskTypeCounts';
|
||||
import { getChartColors, TASK_TYPE_COLORS } from '@/lib/chartTheme';
|
||||
|
||||
export interface TaskTypeBarChartProps {
|
||||
/** Task type count data */
|
||||
data: TaskTypeCount[];
|
||||
/** Optional CSS class name */
|
||||
className?: string;
|
||||
/** Chart height (default: 300) */
|
||||
height?: number;
|
||||
/** Optional label for the chart */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom tooltip component for the bar chart
|
||||
*/
|
||||
function CustomTooltip({ active, payload }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
const { type, count, percentage } = payload[0].payload;
|
||||
const displayName = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
return (
|
||||
<div className="rounded bg-card p-3 shadow-md border border-border">
|
||||
<p className="text-sm font-medium text-foreground">{displayName}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{count} tasks ({Math.round(percentage || 0)}%)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskTypeBarChart - Visualizes task type distribution
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, isLoading } = useTaskTypeCounts();
|
||||
* return <TaskTypeBarChart data={data} />;
|
||||
* ```
|
||||
*/
|
||||
export function TaskTypeBarChart({
|
||||
data,
|
||||
className = '',
|
||||
height = 300,
|
||||
title,
|
||||
}: TaskTypeBarChartProps) {
|
||||
const colors = useMemo(() => getChartColors(), []);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
displayName: item.type.charAt(0).toUpperCase() + item.type.slice(1),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
const barColors = useMemo(() => {
|
||||
return chartData.map((item) => {
|
||||
const colorKey = TASK_TYPE_COLORS[item.type] || 'muted';
|
||||
return colors[colorKey];
|
||||
});
|
||||
}, [chartData, colors]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full ${className}`}
|
||||
role="img"
|
||||
aria-label="Task type bar chart showing distribution of task types"
|
||||
>
|
||||
{title && <h3 className="text-lg font-semibold text-foreground mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
|
||||
accessibilityLayer
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={colors.muted} />
|
||||
<XAxis
|
||||
dataKey="displayName"
|
||||
stroke={colors.muted}
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
<YAxis stroke={colors.muted} style={{ fontSize: '12px' }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '14px' }}
|
||||
formatter={() => 'Task Count'}
|
||||
/>
|
||||
<Bar dataKey="count" radius={[8, 8, 0, 0]}>
|
||||
{barColors.map((color, index) => (
|
||||
<Cell key={`cell-${index}`} fill={color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TaskTypeBarChart;
|
||||
110
ccw/frontend/src/components/charts/WorkflowStatusPieChart.tsx
Normal file
110
ccw/frontend/src/components/charts/WorkflowStatusPieChart.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
// ========================================
|
||||
// WorkflowStatusPieChart Component
|
||||
// ========================================
|
||||
// Recharts pie chart visualizing workflow status distribution
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import type { WorkflowStatusCount } from '@/hooks/useWorkflowStatusCounts';
|
||||
import { getChartColors, STATUS_COLORS } from '@/lib/chartTheme';
|
||||
|
||||
export interface WorkflowStatusPieChartProps {
|
||||
/** Workflow status count data */
|
||||
data: WorkflowStatusCount[];
|
||||
/** Optional CSS class name */
|
||||
className?: string;
|
||||
/** Chart height (default: 300) */
|
||||
height?: number;
|
||||
/** Optional label for the chart */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom tooltip component for the pie chart
|
||||
*/
|
||||
function CustomTooltip({ active, payload }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
const { name, value, payload: data } = payload[0];
|
||||
const percentage = data.percentage ?? Math.round((value / 100) * 100);
|
||||
return (
|
||||
<div className="rounded bg-card p-2 shadow-md border border-border">
|
||||
<p className="text-sm font-medium text-foreground">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{value} ({percentage}%)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkflowStatusPieChart - Visualizes workflow status distribution
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, isLoading } = useWorkflowStatusCounts();
|
||||
* return <WorkflowStatusPieChart data={data} />;
|
||||
* ```
|
||||
*/
|
||||
export function WorkflowStatusPieChart({
|
||||
data,
|
||||
className = '',
|
||||
height = 300,
|
||||
title,
|
||||
}: WorkflowStatusPieChartProps) {
|
||||
const colors = useMemo(() => getChartColors(), []);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
displayName: item.status.charAt(0).toUpperCase() + item.status.slice(1).replace('_', ' '),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
const sliceColors = useMemo(() => {
|
||||
return chartData.map((item) => {
|
||||
const colorKey = STATUS_COLORS[item.status];
|
||||
return colors[colorKey];
|
||||
});
|
||||
}, [chartData, colors]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full ${className}`}
|
||||
role="img"
|
||||
aria-label="Workflow status pie chart showing distribution of workflow statuses"
|
||||
>
|
||||
{title && <h3 className="text-lg font-semibold text-foreground mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<PieChart
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
accessibilityLayer
|
||||
>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ displayName, percentage }) => `${displayName} ${Math.round(percentage || 0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="count"
|
||||
>
|
||||
{sliceColors.map((color, index) => (
|
||||
<Cell key={`cell-${index}`} fill={color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
formatter={(_value, entry: any) => entry.payload.displayName}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowStatusPieChart;
|
||||
18
ccw/frontend/src/components/charts/index.ts
Normal file
18
ccw/frontend/src/components/charts/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// ========================================
|
||||
// Chart Components Exports
|
||||
// ========================================
|
||||
|
||||
export { WorkflowStatusPieChart } from './WorkflowStatusPieChart';
|
||||
export type { WorkflowStatusPieChartProps } from './WorkflowStatusPieChart';
|
||||
|
||||
export { ActivityLineChart } from './ActivityLineChart';
|
||||
export type { ActivityLineChartProps } from './ActivityLineChart';
|
||||
|
||||
export { TaskTypeBarChart } from './TaskTypeBarChart';
|
||||
export type { TaskTypeBarChartProps } from './TaskTypeBarChart';
|
||||
|
||||
export { Sparkline } from './Sparkline';
|
||||
export type { SparklineProps } from './Sparkline';
|
||||
|
||||
export { ChartSkeleton } from './ChartSkeleton';
|
||||
export type { ChartSkeletonProps } from './ChartSkeleton';
|
||||
Reference in New Issue
Block a user