feat: initialize monorepo with package.json for CCW workflow platform

This commit is contained in:
catlog22
2026-02-03 14:42:20 +08:00
parent 5483a72e9f
commit 39b80b3386
267 changed files with 99597 additions and 2658 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View 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';