claude-team-dashboard 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-team-dashboard might be problematic. Click here for more details.

Files changed (49) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +722 -0
  4. package/cleanup.js +73 -0
  5. package/config.js +50 -0
  6. package/dist/assets/icons-Ijf8rQIc.js +1 -0
  7. package/dist/assets/index-Cqc1m1x_.css +1 -0
  8. package/dist/assets/index-jGy3ms0W.js +9 -0
  9. package/dist/assets/react-vendor-DbmSkCAF.js +1 -0
  10. package/dist/index.html +16 -0
  11. package/index.html +13 -0
  12. package/package.json +93 -0
  13. package/server.js +953 -0
  14. package/src/App.jsx +372 -0
  15. package/src/animations-enhanced.css +929 -0
  16. package/src/animations.css +783 -0
  17. package/src/components/ActivityFeed.jsx +289 -0
  18. package/src/components/AgentActivity.jsx +104 -0
  19. package/src/components/AgentCard.jsx +163 -0
  20. package/src/components/AgentOutputViewer.jsx +334 -0
  21. package/src/components/ArchiveViewer.jsx +283 -0
  22. package/src/components/ConnectionStatus.jsx +124 -0
  23. package/src/components/DetailedTaskProgress.jsx +126 -0
  24. package/src/components/ErrorBoundary.jsx +132 -0
  25. package/src/components/Header.jsx +154 -0
  26. package/src/components/LiveAgentStream.jsx +176 -0
  27. package/src/components/LiveCommunication.jsx +326 -0
  28. package/src/components/LiveMetrics.jsx +100 -0
  29. package/src/components/RealTimeMessages.jsx +298 -0
  30. package/src/components/SkeletonLoader.jsx +384 -0
  31. package/src/components/StatsOverview.jsx +209 -0
  32. package/src/components/SystemStatus.jsx +57 -0
  33. package/src/components/TaskList.jsx +306 -0
  34. package/src/components/TeamCard.jsx +126 -0
  35. package/src/components/TeamHistory.jsx +204 -0
  36. package/src/components/__tests__/ConnectionStatus.test.jsx +54 -0
  37. package/src/components/__tests__/StatsOverview.test.jsx +66 -0
  38. package/src/config/constants.js +59 -0
  39. package/src/hooks/useCounterAnimation.js +219 -0
  40. package/src/hooks/useWebSocket.js +76 -0
  41. package/src/index.css +1818 -0
  42. package/src/main.jsx +17 -0
  43. package/src/polish-enhancements.css +303 -0
  44. package/src/premium-visual-polish.css +830 -0
  45. package/src/responsive-enhancements.css +666 -0
  46. package/src/styles/theme.css +395 -0
  47. package/src/test/setup.js +19 -0
  48. package/start.js +36 -0
  49. package/vite.config.js +37 -0
@@ -0,0 +1,209 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Users, ListTodo, Clock, CheckCircle, AlertCircle } from 'lucide-react';
4
+ import { useCounterAnimation } from '../hooks/useCounterAnimation';
5
+
6
+ export function StatsOverview({ stats }) {
7
+ const [isVisible, setIsVisible] = useState(false);
8
+
9
+ useEffect(() => {
10
+ setIsVisible(true);
11
+ }, []);
12
+
13
+ // Use optimized counter animation hook for each stat
14
+ // Hook returns a number value, not a function (false positive)
15
+ const animatedTotalTeams = useCounterAnimation(stats?.totalTeams ?? 0, 1000); // lgtm[js/invocation-of-non-function]
16
+ const animatedTotalAgents = useCounterAnimation(stats?.totalAgents ?? 0, 1000); // lgtm[js/invocation-of-non-function]
17
+ const animatedTotalTasks = useCounterAnimation(stats?.totalTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
18
+ const animatedPendingTasks = useCounterAnimation(stats?.pendingTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
19
+ const animatedInProgress = useCounterAnimation(stats?.inProgressTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
20
+ const animatedCompleted = useCounterAnimation(stats?.completedTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
21
+ const animatedBlocked = useCounterAnimation(stats?.blockedTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
22
+
23
+ if (!stats) return null;
24
+
25
+ // Map stat keys to their animated values
26
+ const animatedValuesMap = {
27
+ totalTeams: animatedTotalTeams,
28
+ totalAgents: animatedTotalAgents,
29
+ totalTasks: animatedTotalTasks,
30
+ pendingTasks: animatedPendingTasks,
31
+ inProgressTasks: animatedInProgress,
32
+ completedTasks: animatedCompleted,
33
+ blockedTasks: animatedBlocked
34
+ };
35
+
36
+ const statCards = [
37
+ {
38
+ label: 'Active Teams',
39
+ key: 'totalTeams',
40
+ icon: Users,
41
+ gradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(37, 99, 235, 0.15) 100%)',
42
+ iconColor: '#60a5fa',
43
+ glowColor: 'rgba(59, 130, 246, 0.4)',
44
+ borderColor: 'rgba(59, 130, 246, 0.3)'
45
+ },
46
+ {
47
+ label: 'Total Agents',
48
+ key: 'totalAgents',
49
+ icon: Users,
50
+ gradient: 'linear-gradient(135deg, rgba(168, 85, 247, 0.25) 0%, rgba(147, 51, 234, 0.15) 100%)',
51
+ iconColor: '#c084fc',
52
+ glowColor: 'rgba(168, 85, 247, 0.4)',
53
+ borderColor: 'rgba(168, 85, 247, 0.3)'
54
+ },
55
+ {
56
+ label: 'Total Tasks',
57
+ key: 'totalTasks',
58
+ icon: ListTodo,
59
+ gradient: 'linear-gradient(135deg, rgba(6, 182, 212, 0.25) 0%, rgba(14, 165, 233, 0.15) 100%)',
60
+ iconColor: '#22d3ee',
61
+ glowColor: 'rgba(6, 182, 212, 0.4)',
62
+ borderColor: 'rgba(6, 182, 212, 0.3)'
63
+ },
64
+ {
65
+ label: 'In Progress',
66
+ key: 'inProgressTasks',
67
+ icon: Clock,
68
+ gradient: 'linear-gradient(135deg, rgba(249, 115, 22, 0.25) 0%, rgba(251, 146, 60, 0.15) 100%)',
69
+ iconColor: '#fb923c',
70
+ glowColor: 'rgba(249, 115, 22, 0.4)',
71
+ borderColor: 'rgba(249, 115, 22, 0.3)'
72
+ },
73
+ {
74
+ label: 'Completed',
75
+ key: 'completedTasks',
76
+ icon: CheckCircle,
77
+ gradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.25) 0%, rgba(21, 128, 61, 0.15) 100%)',
78
+ iconColor: '#4ade80',
79
+ glowColor: 'rgba(34, 197, 94, 0.4)',
80
+ borderColor: 'rgba(34, 197, 94, 0.3)'
81
+ },
82
+ {
83
+ label: 'Blocked',
84
+ key: 'blockedTasks',
85
+ icon: AlertCircle,
86
+ gradient: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(220, 38, 38, 0.15) 100%)',
87
+ iconColor: '#f87171',
88
+ glowColor: 'rgba(239, 68, 68, 0.4)',
89
+ borderColor: 'rgba(239, 68, 68, 0.3)'
90
+ }
91
+ ];
92
+
93
+ return (
94
+ <div
95
+ className="rounded-2xl p-6 mb-6"
96
+ style={{
97
+ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.9) 100%)',
98
+ border: '1px solid rgba(249, 115, 22, 0.15)',
99
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
100
+ backdropFilter: 'blur(16px)'
101
+ }}
102
+ >
103
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
104
+ {statCards.map((stat, index) => {
105
+ const Icon = stat.icon;
106
+ const value = animatedValuesMap[stat.key];
107
+
108
+ return (
109
+ <div
110
+ key={stat.key}
111
+ className="relative group rounded-xl p-4 transition-all duration-300"
112
+ style={{
113
+ background: stat.gradient,
114
+ border: `1px solid ${stat.borderColor}`,
115
+ boxShadow: `0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08)`,
116
+ opacity: isVisible ? 1 : 0,
117
+ transform: isVisible ? 'translateY(0)' : 'translateY(10px)',
118
+ transitionDelay: `${index * 80}ms`
119
+ }}
120
+ onMouseEnter={(e) => {
121
+ e.currentTarget.style.transform = 'translateY(-4px) scale(1.02)';
122
+ e.currentTarget.style.boxShadow = `0 8px 24px ${stat.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.12)`;
123
+ e.currentTarget.style.borderColor = stat.borderColor;
124
+ }}
125
+ onMouseLeave={(e) => {
126
+ e.currentTarget.style.transform = 'translateY(0) scale(1)';
127
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08)';
128
+ e.currentTarget.style.borderColor = stat.borderColor;
129
+ }}
130
+ >
131
+ {/* Icon */}
132
+ <div
133
+ className="inline-flex p-2.5 rounded-lg mb-3 transition-transform duration-300 group-hover:scale-110"
134
+ style={{
135
+ background: 'rgba(0, 0, 0, 0.2)',
136
+ boxShadow: `0 2px 8px ${stat.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
137
+ border: `1px solid ${stat.borderColor}`
138
+ }}
139
+ >
140
+ <Icon
141
+ className="h-5 w-5"
142
+ style={{
143
+ color: stat.iconColor,
144
+ filter: `drop-shadow(0 0 8px ${stat.glowColor})`
145
+ }}
146
+ />
147
+ </div>
148
+
149
+ {/* Value */}
150
+ <div className="mb-1">
151
+ <p
152
+ className="text-3xl font-extrabold tabular-nums"
153
+ style={{
154
+ color: '#ffffff',
155
+ letterSpacing: '-0.03em',
156
+ textShadow: `0 2px 4px rgba(0, 0, 0, 0.3), 0 0 12px ${stat.glowColor}`,
157
+ lineHeight: 1
158
+ }}
159
+ >
160
+ {value}
161
+ </p>
162
+ </div>
163
+
164
+ {/* Label */}
165
+ <p
166
+ className="text-xs font-semibold uppercase tracking-wider"
167
+ style={{
168
+ color: 'rgba(209, 213, 219, 0.7)',
169
+ letterSpacing: '0.05em'
170
+ }}
171
+ >
172
+ {stat.label}
173
+ </p>
174
+
175
+ {/* Hover Glow Effect */}
176
+ <div
177
+ className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
178
+ style={{
179
+ background: `radial-gradient(circle at center, ${stat.glowColor}, transparent 70%)`
180
+ }}
181
+ />
182
+
183
+ {/* Top Border Accent */}
184
+ <div
185
+ className="absolute top-0 left-0 right-0 h-1 rounded-t-xl"
186
+ style={{
187
+ background: `linear-gradient(90deg, transparent, ${stat.iconColor}, transparent)`,
188
+ opacity: 0.6
189
+ }}
190
+ />
191
+ </div>
192
+ );
193
+ })}
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ StatsOverview.propTypes = {
200
+ stats: PropTypes.shape({
201
+ totalTeams: PropTypes.number,
202
+ totalAgents: PropTypes.number,
203
+ totalTasks: PropTypes.number,
204
+ pendingTasks: PropTypes.number,
205
+ inProgressTasks: PropTypes.number,
206
+ completedTasks: PropTypes.number,
207
+ blockedTasks: PropTypes.number
208
+ })
209
+ };
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { Server, Database, Wifi, Clock } from 'lucide-react';
3
+ import dayjs from 'dayjs';
4
+ import relativeTime from 'dayjs/plugin/relativeTime';
5
+ dayjs.extend(relativeTime);
6
+
7
+ export function SystemStatus({ isConnected, lastUpdate }) {
8
+
9
+ return (
10
+ <div className="card">
11
+ <div className="flex items-center gap-2 mb-4">
12
+ <Server className="h-5 w-5 text-claude-orange" />
13
+ <h3 className="text-lg font-semibold text-white">System Status</h3>
14
+ </div>
15
+
16
+ <div className="space-y-3">
17
+ <div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
18
+ <div className="flex items-center gap-2">
19
+ <Wifi className={`h-4 w-4 ${isConnected ? 'text-green-400' : 'text-red-400'}`} />
20
+ <span className="text-sm text-gray-300">WebSocket</span>
21
+ </div>
22
+ <span className={`text-xs font-semibold ${isConnected ? 'text-green-400' : 'text-red-400'}`}>
23
+ {isConnected ? 'Connected' : 'Disconnected'}
24
+ </span>
25
+ </div>
26
+
27
+ <div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
28
+ <div className="flex items-center gap-2">
29
+ <Server className="h-4 w-4 text-blue-400" />
30
+ <span className="text-sm text-gray-300">Backend API</span>
31
+ </div>
32
+ <span className="text-xs font-semibold text-green-400">Running</span>
33
+ </div>
34
+
35
+ <div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
36
+ <div className="flex items-center gap-2">
37
+ <Database className="h-4 w-4 text-purple-400" />
38
+ <span className="text-sm text-gray-300">File Watchers</span>
39
+ </div>
40
+ <span className="text-xs font-semibold text-green-400">Active</span>
41
+ </div>
42
+
43
+ {lastUpdate && (
44
+ <div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
45
+ <div className="flex items-center gap-2">
46
+ <Clock className="h-4 w-4 text-cyan-400" />
47
+ <span className="text-sm text-gray-300">Last Update</span>
48
+ </div>
49
+ <span className="text-xs font-semibold text-cyan-400">
50
+ {dayjs(new Date(lastUpdate.timestamp || Date.now())).fromNow()}
51
+ </span>
52
+ </div>
53
+ )}
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,306 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Circle, Clock, CheckCircle, AlertCircle, User, ChevronDown, ChevronUp } from 'lucide-react';
4
+
5
+ export function TaskList({ tasks }) {
6
+ const [expandedTasks, setExpandedTasks] = useState(new Set());
7
+
8
+ if (!tasks || tasks.length === 0) {
9
+ return (
10
+ <div
11
+ className="text-center py-12 rounded-2xl"
12
+ style={{
13
+ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(15, 23, 42, 0.5) 100%)',
14
+ border: '1px dashed rgba(156, 163, 175, 0.3)'
15
+ }}
16
+ >
17
+ <Circle className="h-12 w-12 text-gray-600 mx-auto mb-3 opacity-50" />
18
+ <p className="text-gray-400 text-sm">No tasks yet</p>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ const toggleTaskExpanded = (taskId) => {
24
+ setExpandedTasks(prev => {
25
+ const newSet = new Set(prev);
26
+ if (newSet.has(taskId)) {
27
+ newSet.delete(taskId);
28
+ } else {
29
+ newSet.add(taskId);
30
+ }
31
+ return newSet;
32
+ });
33
+ };
34
+
35
+ const getStatusIcon = (status) => {
36
+ switch (status) {
37
+ case 'pending':
38
+ return (
39
+ <Circle
40
+ className="h-6 w-6"
41
+ style={{
42
+ color: '#facc15',
43
+ filter: 'drop-shadow(0 0 6px rgba(250, 204, 21, 0.5))'
44
+ }}
45
+ />
46
+ );
47
+ case 'in_progress':
48
+ return (
49
+ <Clock
50
+ className="h-6 w-6 animate-spin"
51
+ style={{
52
+ color: '#60a5fa',
53
+ filter: 'drop-shadow(0 0 8px rgba(96, 165, 250, 0.5))',
54
+ animationDuration: '3s'
55
+ }}
56
+ />
57
+ );
58
+ case 'completed':
59
+ return (
60
+ <CheckCircle
61
+ className="h-6 w-6"
62
+ style={{
63
+ color: '#4ade80',
64
+ filter: 'drop-shadow(0 0 8px rgba(74, 222, 128, 0.5))'
65
+ }}
66
+ />
67
+ );
68
+ default:
69
+ return <Circle className="h-6 w-6 text-gray-400" />;
70
+ }
71
+ };
72
+
73
+ const getStatusConfig = (status, blockedBy) => {
74
+ if (blockedBy && blockedBy.length > 0) {
75
+ return {
76
+ label: 'Blocked',
77
+ bgGradient: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(220, 38, 38, 0.15) 100%)',
78
+ textColor: '#f87171',
79
+ borderColor: 'rgba(239, 68, 68, 0.4)',
80
+ glowColor: 'rgba(239, 68, 68, 0.3)'
81
+ };
82
+ }
83
+
84
+ switch (status) {
85
+ case 'pending':
86
+ return {
87
+ label: 'Pending',
88
+ bgGradient: 'linear-gradient(135deg, rgba(234, 179, 8, 0.2) 0%, rgba(202, 138, 4, 0.12) 100%)',
89
+ textColor: '#facc15',
90
+ borderColor: 'rgba(234, 179, 8, 0.4)',
91
+ glowColor: 'rgba(234, 179, 8, 0.3)'
92
+ };
93
+ case 'in_progress':
94
+ return {
95
+ label: 'In Progress',
96
+ bgGradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(37, 99, 235, 0.15) 100%)',
97
+ textColor: '#60a5fa',
98
+ borderColor: 'rgba(59, 130, 246, 0.5)',
99
+ glowColor: 'rgba(59, 130, 246, 0.35)'
100
+ };
101
+ case 'completed':
102
+ return {
103
+ label: 'Completed',
104
+ bgGradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.25) 0%, rgba(21, 128, 61, 0.15) 100%)',
105
+ textColor: '#4ade80',
106
+ borderColor: 'rgba(34, 197, 94, 0.5)',
107
+ glowColor: 'rgba(34, 197, 94, 0.3)'
108
+ };
109
+ default:
110
+ return {
111
+ label: 'Unknown',
112
+ bgGradient: 'rgba(55, 65, 81, 0.3)',
113
+ textColor: '#9ca3af',
114
+ borderColor: 'rgba(75, 85, 99, 0.4)',
115
+ glowColor: 'rgba(75, 85, 99, 0.2)'
116
+ };
117
+ }
118
+ };
119
+
120
+ return (
121
+ <div className="space-y-3">
122
+ {tasks.map((task, index) => {
123
+ const statusConfig = getStatusConfig(task.status, task.blockedBy);
124
+ const isExpanded = expandedTasks.has(task.id || index);
125
+ const hasDescription = task.description && task.description.length > 100;
126
+
127
+ return (
128
+ <div
129
+ key={task.id || index}
130
+ className="group relative rounded-2xl p-5 transition-all duration-300"
131
+ style={{
132
+ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.6) 0%, rgba(15, 23, 42, 0.5) 100%)',
133
+ border: '1px solid rgba(75, 85, 99, 0.3)',
134
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
135
+ animationDelay: `${index * 60}ms`
136
+ }}
137
+ onMouseEnter={(e) => {
138
+ e.currentTarget.style.transform = 'translateX(6px)';
139
+ e.currentTarget.style.borderColor = statusConfig.borderColor;
140
+ e.currentTarget.style.boxShadow = `0 6px 20px ${statusConfig.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.08)`;
141
+ }}
142
+ onMouseLeave={(e) => {
143
+ e.currentTarget.style.transform = 'translateX(0)';
144
+ e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.3)';
145
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)';
146
+ }}
147
+ >
148
+ {/* Status Bar */}
149
+ <div
150
+ className="absolute left-0 top-0 bottom-0 w-1 rounded-l-2xl"
151
+ style={{
152
+ background: statusConfig.bgGradient,
153
+ boxShadow: `0 0 12px ${statusConfig.glowColor}`
154
+ }}
155
+ />
156
+
157
+ <div className="flex items-start gap-4">
158
+ {/* Status Icon */}
159
+ <div className="mt-0.5 flex-shrink-0">
160
+ {getStatusIcon(task.status)}
161
+ </div>
162
+
163
+ {/* Task Content */}
164
+ <div className="flex-1 min-w-0">
165
+ {/* Header */}
166
+ <div className="flex items-start justify-between gap-3 mb-3">
167
+ <h5
168
+ className="text-white font-bold text-base leading-tight"
169
+ style={{
170
+ letterSpacing: '-0.01em',
171
+ textShadow: '0 1px 2px rgba(0, 0, 0, 0.3)'
172
+ }}
173
+ >
174
+ {task.subject}
175
+ </h5>
176
+
177
+ {/* Status Badge */}
178
+ <span
179
+ className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider flex-shrink-0"
180
+ style={{
181
+ background: statusConfig.bgGradient,
182
+ color: statusConfig.textColor,
183
+ border: `1px solid ${statusConfig.borderColor}`,
184
+ boxShadow: `0 2px 8px ${statusConfig.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.15)`,
185
+ textShadow: `0 0 10px ${statusConfig.glowColor}`
186
+ }}
187
+ >
188
+ {statusConfig.label}
189
+ </span>
190
+ </div>
191
+
192
+ {/* Description */}
193
+ {task.description && (
194
+ <div className="mb-3">
195
+ <p
196
+ className={`text-sm leading-relaxed ${!isExpanded && hasDescription ? 'line-clamp-2' : ''}`}
197
+ style={{
198
+ color: 'rgba(209, 213, 219, 0.85)',
199
+ letterSpacing: '-0.01em'
200
+ }}
201
+ >
202
+ {task.description}
203
+ </p>
204
+ {hasDescription && (
205
+ <button
206
+ onClick={() => toggleTaskExpanded(task.id || index)}
207
+ className="flex items-center gap-1 text-xs font-medium mt-2 transition-colors duration-200"
208
+ style={{
209
+ color: statusConfig.textColor
210
+ }}
211
+ aria-label={isExpanded ? "Show less description" : "Show full description"}
212
+ aria-expanded={isExpanded}
213
+ >
214
+ {isExpanded ? (
215
+ <>
216
+ <ChevronUp className="h-3 w-3" />
217
+ Show less
218
+ </>
219
+ ) : (
220
+ <>
221
+ <ChevronDown className="h-3 w-3" />
222
+ Read more
223
+ </>
224
+ )}
225
+ </button>
226
+ )}
227
+ </div>
228
+ )}
229
+
230
+ {/* Tags and Metadata */}
231
+ <div className="flex flex-wrap gap-2">
232
+ {task.owner && (
233
+ <div
234
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
235
+ style={{
236
+ background: 'rgba(59, 130, 246, 0.15)',
237
+ color: '#93c5fd',
238
+ border: '1px solid rgba(59, 130, 246, 0.3)'
239
+ }}
240
+ >
241
+ <User className="h-3.5 w-3.5" />
242
+ <span>{task.owner}</span>
243
+ </div>
244
+ )}
245
+
246
+ {task.blockedBy && task.blockedBy.length > 0 && (
247
+ <div
248
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
249
+ style={{
250
+ background: 'rgba(239, 68, 68, 0.15)',
251
+ color: '#fca5a5',
252
+ border: '1px solid rgba(239, 68, 68, 0.35)',
253
+ boxShadow: '0 0 12px rgba(239, 68, 68, 0.2)'
254
+ }}
255
+ >
256
+ <AlertCircle className="h-3.5 w-3.5" />
257
+ <span>Blocked by {task.blockedBy.length}</span>
258
+ </div>
259
+ )}
260
+
261
+ {task.blocks && task.blocks.length > 0 && (
262
+ <div
263
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
264
+ style={{
265
+ background: 'rgba(249, 115, 22, 0.15)',
266
+ color: '#fdba74',
267
+ border: '1px solid rgba(249, 115, 22, 0.3)'
268
+ }}
269
+ >
270
+ <AlertCircle className="h-3.5 w-3.5" />
271
+ <span>Blocks {task.blocks.length}</span>
272
+ </div>
273
+ )}
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ {/* Hover Shine Effect */}
279
+ <div
280
+ className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
281
+ style={{
282
+ background: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.03) 50%, transparent 100%)'
283
+ }}
284
+ />
285
+ </div>
286
+ );
287
+ })}
288
+ </div>
289
+ );
290
+ }
291
+
292
+ TaskList.propTypes = {
293
+ tasks: PropTypes.arrayOf(
294
+ PropTypes.shape({
295
+ id: PropTypes.string,
296
+ subject: PropTypes.string.isRequired,
297
+ description: PropTypes.string,
298
+ status: PropTypes.oneOf(['pending', 'in_progress', 'completed']).isRequired,
299
+ owner: PropTypes.string,
300
+ blockedBy: PropTypes.array,
301
+ blocks: PropTypes.array,
302
+ createdAt: PropTypes.number,
303
+ updatedAt: PropTypes.number
304
+ })
305
+ )
306
+ };