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,298 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { MessageCircle, ArrowRight, Radio } from 'lucide-react';
4
+ import dayjs from 'dayjs';
5
+ import relativeTime from 'dayjs/plugin/relativeTime';
6
+ dayjs.extend(relativeTime);
7
+
8
+ // Convert technical messages to natural language
9
+ const parseMessageToNatural = (text, summary) => {
10
+ // If there's a clear summary, use it
11
+ if (summary && !summary.includes('{') && !summary.includes('idle_notification')) {
12
+ // Determine type from summary content
13
+ let type = 'status';
14
+ if (summary.toLowerCase().includes('completed') || summary.includes('✓') || summary.includes('✅')) {
15
+ type = 'completion';
16
+ } else if (summary.toLowerCase().includes('question') || summary.includes('?')) {
17
+ type = 'question';
18
+ } else if (summary.toLowerCase().includes('coordin') || summary.toLowerCase().includes('discuss') || summary.toLowerCase().includes('help')) {
19
+ type = 'coordination';
20
+ }
21
+ return { text: summary, type };
22
+ }
23
+
24
+ // Try to parse as JSON
25
+ try {
26
+ const parsed = JSON.parse(text);
27
+
28
+ switch (parsed.type) {
29
+ case 'idle_notification':
30
+ return {
31
+ text: parsed.lastTaskSubject
32
+ ? `💤 Finished "${parsed.lastTaskSubject}" - ready for next task`
33
+ : '💤 Available and waiting for assignment',
34
+ type: 'status'
35
+ };
36
+
37
+ case 'task_completed':
38
+ return {
39
+ text: `✅ Completed: ${parsed.taskSubject || 'Task'}`,
40
+ type: 'completion'
41
+ };
42
+
43
+ case 'task_assigned':
44
+ return {
45
+ text: `📋 Started working on: ${parsed.taskSubject || 'New task'}`,
46
+ type: 'status'
47
+ };
48
+
49
+ case 'question':
50
+ return {
51
+ text: `❓ ${parsed.message || parsed.content || 'Question raised'}`,
52
+ type: 'question'
53
+ };
54
+
55
+ case 'coordination':
56
+ return {
57
+ text: `🤝 ${parsed.message || parsed.content || 'Coordinating with team'}`,
58
+ type: 'coordination'
59
+ };
60
+
61
+ default:
62
+ return {
63
+ text: parsed.message || parsed.content || 'Message received',
64
+ type: 'status'
65
+ };
66
+ }
67
+ } catch (e) {
68
+ // Not JSON, use as-is
69
+ if (!text || text.trim() === '') {
70
+ return { text: '👋 Said hello', type: 'status' };
71
+ }
72
+
73
+ // Truncate if too long
74
+ if (text.length > 200) {
75
+ return {
76
+ text: text.substring(0, 150) + '...',
77
+ type: 'status'
78
+ };
79
+ }
80
+
81
+ return { text, type: 'status' };
82
+ }
83
+ };
84
+
85
+ export function RealTimeMessages({ teams }) {
86
+ const [messages, setMessages] = useState([]);
87
+ const [filter, setFilter] = useState('all');
88
+ const [loading, setLoading] = useState(false);
89
+ const [error, setError] = useState(null);
90
+
91
+ useEffect(() => {
92
+ // Fetch real inbox messages from all teams
93
+ if (!teams || teams.length === 0) return;
94
+
95
+ const fetchAllMessages = async () => {
96
+ try {
97
+ setLoading(true);
98
+ setError(null);
99
+
100
+ const allMessages = [];
101
+
102
+ // Fetch messages from each team
103
+ for (const team of teams) {
104
+ try {
105
+ const response = await fetch(`http://localhost:3001/api/teams/${encodeURIComponent(team.name)}/inboxes`);
106
+
107
+ if (!response.ok) {
108
+ console.warn(`Failed to fetch messages for team ${team.name}: ${response.status}`);
109
+ continue;
110
+ }
111
+
112
+ const data = await response.json();
113
+
114
+ // Convert inbox data to message format
115
+ if (data.inboxes && typeof data.inboxes === 'object') {
116
+ Object.entries(data.inboxes).forEach(([agentName, inbox]) => {
117
+ if (inbox.messages && Array.isArray(inbox.messages)) {
118
+ inbox.messages.forEach(msg => {
119
+ // Convert to natural language
120
+ const naturalMsg = parseMessageToNatural(msg.text, msg.summary);
121
+
122
+ allMessages.push({
123
+ id: `${team.name}-${agentName}-${msg.timestamp}-${Math.random()}`,
124
+ from: msg.from || agentName,
125
+ to: agentName,
126
+ team: team.name,
127
+ type: naturalMsg.type,
128
+ message: naturalMsg.text,
129
+ timestamp: new Date(msg.timestamp),
130
+ color: msg.color || 'blue',
131
+ read: msg.read || false
132
+ });
133
+ });
134
+ }
135
+ });
136
+ }
137
+ } catch (err) {
138
+ console.error(`Error fetching messages for team ${team.name}:`, err);
139
+ }
140
+ }
141
+
142
+ // Sort by timestamp (newest first)
143
+ allMessages.sort((a, b) => b.timestamp - a.timestamp);
144
+
145
+ // Keep last 100 messages
146
+ setMessages(allMessages.slice(0, 100));
147
+ setLoading(false);
148
+ } catch (err) {
149
+ console.error('Error fetching messages:', err);
150
+ setError(err.message);
151
+ setLoading(false);
152
+ }
153
+ };
154
+
155
+ // Fetch immediately
156
+ fetchAllMessages();
157
+
158
+ // Then poll every 5 seconds
159
+ const interval = setInterval(fetchAllMessages, 5000);
160
+
161
+ return () => clearInterval(interval);
162
+ }, [teams]);
163
+
164
+ const filteredMessages = filter === 'all'
165
+ ? messages
166
+ : messages.filter(m => m.type === filter);
167
+
168
+ const getMessageColor = (type) => {
169
+ switch (type) {
170
+ case 'status': return 'message-status';
171
+ case 'completion': return 'message-completion';
172
+ case 'coordination': return 'message-coordination';
173
+ case 'question': return 'message-question';
174
+ default: return 'border-gray-600 bg-gray-700/30';
175
+ }
176
+ };
177
+
178
+ const getTypeIcon = (type) => {
179
+ switch (type) {
180
+ case 'status': return '📊';
181
+ case 'completion': return '✅';
182
+ case 'coordination': return '🤝';
183
+ case 'question': return '❓';
184
+ default: return '💬';
185
+ }
186
+ };
187
+
188
+ return (
189
+ <div className="card" style={{ height: '600px', display: 'flex', flexDirection: 'column' }}>
190
+ <div className="flex items-center justify-between mb-4">
191
+ <div className="flex items-center gap-2">
192
+ <Radio className="h-5 w-5 text-claude-orange" />
193
+ <h3 className="text-lg font-semibold text-white">Agent Inter-Communication</h3>
194
+ </div>
195
+ <span className="live-stats-indicator">
196
+ <span className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></span>
197
+ {messages.length} {messages.length === 1 ? 'message' : 'messages'}
198
+ </span>
199
+ </div>
200
+
201
+ {/* Filter Tabs */}
202
+ <div className="flex gap-2 mb-4 overflow-x-auto pb-1">
203
+ {['all', 'status', 'completion', 'coordination', 'question'].map(f => (
204
+ <button
205
+ key={f}
206
+ onClick={() => setFilter(f)}
207
+ className={`filter-button whitespace-nowrap ${
208
+ filter === f ? 'filter-button-active' : 'filter-button-inactive'
209
+ }`}
210
+ >
211
+ {f.charAt(0).toUpperCase() + f.slice(1)}
212
+ </button>
213
+ ))}
214
+ </div>
215
+
216
+ {/* Messages Stream */}
217
+ <div className="flex-1 overflow-y-auto space-y-2" style={{ minHeight: 0 }}>
218
+ {error ? (
219
+ <div className="text-center py-12 text-red-400">
220
+ <MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50" />
221
+ <p className="text-sm">Error loading messages</p>
222
+ <p className="text-xs mt-1">{error}</p>
223
+ </div>
224
+ ) : loading && filteredMessages.length === 0 ? (
225
+ <div className="text-center py-12 text-gray-400">
226
+ <MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50 animate-pulse" />
227
+ <p className="text-sm">Loading messages...</p>
228
+ </div>
229
+ ) : filteredMessages.length === 0 ? (
230
+ <div className="text-center py-12 text-gray-400">
231
+ <MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50" />
232
+ <p className="text-sm">No messages yet</p>
233
+ <p className="text-xs mt-1">Agent communication will stream here in real-time</p>
234
+ </div>
235
+ ) : (
236
+ filteredMessages.map(msg => (
237
+ <div
238
+ key={msg.id}
239
+ className={`message-card p-3.5 rounded-xl border transition-all ${getMessageColor(msg.type)}`}
240
+ style={{ animation: 'fadeIn 0.3s ease-out' }}
241
+ >
242
+ <div className="flex items-start gap-3">
243
+ <span className="message-emoji text-2xl">{getTypeIcon(msg.type)}</span>
244
+ <div className="flex-1 min-w-0">
245
+ <div className="flex items-center gap-2 mb-1.5 flex-wrap">
246
+ <span className="agent-badge">{msg.from}</span>
247
+ <ArrowRight className="message-arrow h-3.5 w-3.5" />
248
+ <span className="text-sm text-gray-300 font-medium">{msg.to}</span>
249
+ <span className="message-timestamp ml-auto">
250
+ {dayjs(msg.timestamp).fromNow()}
251
+ </span>
252
+ </div>
253
+ <p className="message-text">{msg.message}</p>
254
+ {msg.team && (
255
+ <div className="mt-2 flex items-center gap-2">
256
+ <span className="text-xs text-gray-400 font-medium">Team: {msg.team}</span>
257
+ {!msg.read && (
258
+ <span className="unread-badge">New</span>
259
+ )}
260
+ </div>
261
+ )}
262
+ </div>
263
+ </div>
264
+ </div>
265
+ ))
266
+ )}
267
+ </div>
268
+
269
+ {/* Stats Footer */}
270
+ <div className="pt-4 mt-4 border-t border-gray-700">
271
+ <div className="grid grid-cols-2 gap-4 text-center">
272
+ <div>
273
+ <div className="text-xl font-bold text-blue-400">
274
+ {messages.filter(m => m.type === 'status').length}
275
+ </div>
276
+ <div className="text-xs text-gray-400">Status Updates</div>
277
+ </div>
278
+ <div>
279
+ <div className="text-xl font-bold text-green-400">
280
+ {messages.filter(m => m.type === 'completion').length}
281
+ </div>
282
+ <div className="text-xs text-gray-400">Completions</div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ }
289
+
290
+ RealTimeMessages.propTypes = {
291
+ teams: PropTypes.arrayOf(
292
+ PropTypes.shape({
293
+ name: PropTypes.string,
294
+ config: PropTypes.object,
295
+ tasks: PropTypes.array
296
+ })
297
+ )
298
+ };
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Skeleton Loader Components
3
+ *
4
+ * Animated placeholder components for loading states
5
+ * Provides better UX than spinners by showing content structure
6
+ *
7
+ * Usage: Replace spinner/loading states with appropriate skeleton component
8
+ */
9
+
10
+ import React from 'react';
11
+
12
+ /**
13
+ * SkeletonCard - Full card skeleton
14
+ * Use for: TeamCard, SystemStatus, etc.
15
+ */
16
+ export function SkeletonCard() {
17
+ return (
18
+ <div className="card p-6 space-y-4">
19
+ <div className="flex items-center gap-4">
20
+ <div className="skeleton-animated w-16 h-16 rounded-full flex-shrink-0" />
21
+ <div className="flex-1 space-y-2">
22
+ <div className="skeleton-animated h-6 w-3/4" />
23
+ <div className="skeleton-animated h-4 w-1/2" />
24
+ </div>
25
+ </div>
26
+ <div className="space-y-2">
27
+ <div className="skeleton-animated h-4 w-full" />
28
+ <div className="skeleton-animated h-4 w-5/6" />
29
+ <div className="skeleton-animated h-4 w-4/5" />
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ /**
36
+ * SkeletonStat - Stat card skeleton
37
+ * Use for: StatsOverview items
38
+ */
39
+ export function SkeletonStat() {
40
+ return (
41
+ <div className="flex items-center gap-3">
42
+ <div className="skeleton-animated w-12 h-12 rounded-lg flex-shrink-0" />
43
+ <div className="space-y-2 flex-1">
44
+ <div className="skeleton-animated h-3 w-20" />
45
+ <div className="skeleton-animated h-6 w-12" />
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * SkeletonStatsOverview - Full stats overview skeleton
53
+ * Use for: Main stats bar at top of dashboard
54
+ */
55
+ export function SkeletonStatsOverview() {
56
+ return (
57
+ <div className="card p-4 mb-4">
58
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
59
+ {[...Array(6)].map((_, i) => (
60
+ <SkeletonStat key={i} />
61
+ ))}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ /**
68
+ * SkeletonTaskItem - Single task item skeleton
69
+ * Use for: TaskList items
70
+ */
71
+ export function SkeletonTaskItem() {
72
+ return (
73
+ <div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600">
74
+ <div className="flex items-start gap-3">
75
+ <div className="skeleton-animated w-5 h-5 rounded-full mt-1 flex-shrink-0" />
76
+ <div className="flex-1 space-y-3">
77
+ <div className="flex items-start justify-between gap-2">
78
+ <div className="skeleton-animated h-5 w-2/3" />
79
+ <div className="skeleton-animated h-6 w-20 rounded-full" />
80
+ </div>
81
+ <div className="space-y-2">
82
+ <div className="skeleton-animated h-4 w-full" />
83
+ <div className="skeleton-animated h-4 w-4/5" />
84
+ </div>
85
+ <div className="flex gap-2">
86
+ <div className="skeleton-animated h-6 w-24 rounded" />
87
+ <div className="skeleton-animated h-6 w-28 rounded" />
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ /**
96
+ * SkeletonTaskList - Full task list skeleton
97
+ * Use for: TaskList loading state
98
+ */
99
+ export function SkeletonTaskList({ count = 3 }) {
100
+ return (
101
+ <div className="space-y-2">
102
+ {[...Array(count)].map((_, i) => (
103
+ <SkeletonTaskItem key={i} />
104
+ ))}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ /**
110
+ * SkeletonAgentCard - Agent card skeleton
111
+ * Use for: AgentCard loading state
112
+ */
113
+ export function SkeletonAgentCard() {
114
+ return (
115
+ <div className="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
116
+ <div className="flex items-start gap-3">
117
+ <div className="skeleton-animated w-10 h-10 rounded-lg flex-shrink-0" />
118
+ <div className="flex-1 space-y-2">
119
+ <div className="flex items-center gap-2">
120
+ <div className="skeleton-animated h-5 w-32" />
121
+ <div className="skeleton-animated h-5 w-12 rounded-full" />
122
+ </div>
123
+ <div className="skeleton-animated h-4 w-40" />
124
+ <div className="skeleton-animated h-3 w-24" />
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ /**
132
+ * SkeletonTeamCard - Team card skeleton with agents
133
+ * Use for: TeamCard loading state
134
+ */
135
+ export function SkeletonTeamCard() {
136
+ return (
137
+ <div className="card space-y-6">
138
+ {/* Header */}
139
+ <div className="flex items-center justify-between">
140
+ <div className="flex items-center gap-3">
141
+ <div className="skeleton-animated w-12 h-12 rounded-lg" />
142
+ <div className="space-y-2">
143
+ <div className="skeleton-animated h-6 w-40" />
144
+ <div className="skeleton-animated h-4 w-32" />
145
+ </div>
146
+ </div>
147
+ <div className="skeleton-animated h-8 w-24 rounded-full" />
148
+ </div>
149
+
150
+ {/* Agents */}
151
+ <div className="space-y-3">
152
+ <div className="skeleton-animated h-4 w-20" />
153
+ <div className="space-y-2">
154
+ <SkeletonAgentCard />
155
+ <SkeletonAgentCard />
156
+ </div>
157
+ </div>
158
+
159
+ {/* Tasks */}
160
+ <div className="space-y-3">
161
+ <div className="skeleton-animated h-4 w-16" />
162
+ <SkeletonTaskList count={2} />
163
+ </div>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ /**
169
+ * SkeletonActivityItem - Activity feed item skeleton
170
+ * Use for: ActivityFeed items
171
+ */
172
+ export function SkeletonActivityItem() {
173
+ return (
174
+ <div className="flex items-start gap-3 p-3 rounded-lg bg-gray-700/30">
175
+ <div className="skeleton-animated w-8 h-8 rounded-full flex-shrink-0" />
176
+ <div className="flex-1 space-y-2">
177
+ <div className="skeleton-animated h-4 w-full" />
178
+ <div className="skeleton-animated h-3 w-3/4" />
179
+ <div className="skeleton-animated h-3 w-20" />
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ /**
186
+ * SkeletonActivityFeed - Full activity feed skeleton
187
+ * Use for: ActivityFeed loading state
188
+ */
189
+ export function SkeletonActivityFeed({ count = 5 }) {
190
+ return (
191
+ <div className="card">
192
+ <div className="flex items-center justify-between mb-4">
193
+ <div className="skeleton-animated h-6 w-32" />
194
+ <div className="skeleton-animated h-8 w-8 rounded-full" />
195
+ </div>
196
+ <div className="space-y-2 max-h-96 overflow-y-auto">
197
+ {[...Array(count)].map((_, i) => (
198
+ <SkeletonActivityItem key={i} />
199
+ ))}
200
+ </div>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ /**
206
+ * SkeletonChart - Chart/graph skeleton
207
+ * Use for: LiveMetrics, charts
208
+ */
209
+ export function SkeletonChart() {
210
+ return (
211
+ <div className="card p-6 space-y-4">
212
+ <div className="skeleton-animated h-6 w-48" />
213
+ <div className="flex items-end justify-between h-32 gap-2">
214
+ {[...Array(8)].map((_, i) => (
215
+ <div
216
+ key={i}
217
+ className="skeleton-animated flex-1"
218
+ style={{
219
+ height: `${Math.random() * 60 + 40}%`,
220
+ borderRadius: '4px 4px 0 0'
221
+ }}
222
+ />
223
+ ))}
224
+ </div>
225
+ <div className="flex justify-between">
226
+ {[...Array(8)].map((_, i) => (
227
+ <div key={i} className="skeleton-animated h-3 w-8" />
228
+ ))}
229
+ </div>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ /**
235
+ * SkeletonMetricCard - Metric card skeleton
236
+ * Use for: LiveMetrics cards
237
+ */
238
+ export function SkeletonMetricCard() {
239
+ return (
240
+ <div className="stat-card p-6 space-y-4">
241
+ <div className="flex items-center justify-between">
242
+ <div className="skeleton-animated h-5 w-32" />
243
+ <div className="skeleton-animated h-8 w-8 rounded-lg" />
244
+ </div>
245
+ <div className="skeleton-animated h-10 w-20" />
246
+ <div className="skeleton-animated h-4 w-full" />
247
+ </div>
248
+ );
249
+ }
250
+
251
+ /**
252
+ * SkeletonTable - Table skeleton
253
+ * Use for: Data tables
254
+ */
255
+ export function SkeletonTable({ rows = 5, columns = 4 }) {
256
+ return (
257
+ <div className="card overflow-hidden">
258
+ {/* Table header */}
259
+ <div className="flex gap-4 p-4 border-b border-gray-700">
260
+ {[...Array(columns)].map((_, i) => (
261
+ <div key={i} className="flex-1">
262
+ <div className="skeleton-animated h-4 w-3/4" />
263
+ </div>
264
+ ))}
265
+ </div>
266
+ {/* Table rows */}
267
+ <div className="divide-y divide-gray-700">
268
+ {[...Array(rows)].map((_, rowIndex) => (
269
+ <div key={rowIndex} className="flex gap-4 p-4">
270
+ {[...Array(columns)].map((_, colIndex) => (
271
+ <div key={colIndex} className="flex-1">
272
+ <div className="skeleton-animated h-4 w-full" />
273
+ </div>
274
+ ))}
275
+ </div>
276
+ ))}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ /**
283
+ * SkeletonText - Generic text skeleton
284
+ * Use for: Text placeholders
285
+ */
286
+ export function SkeletonText({ lines = 3, widths = ['100%', '90%', '80%'] }) {
287
+ return (
288
+ <div className="space-y-2">
289
+ {[...Array(lines)].map((_, i) => (
290
+ <div
291
+ key={i}
292
+ className="skeleton-animated h-4"
293
+ style={{ width: widths[i % widths.length] }}
294
+ />
295
+ ))}
296
+ </div>
297
+ );
298
+ }
299
+
300
+ /**
301
+ * SkeletonAvatar - Avatar skeleton
302
+ * Use for: User avatars, agent icons
303
+ */
304
+ export function SkeletonAvatar({ size = 'medium' }) {
305
+ const sizeClasses = {
306
+ small: 'w-8 h-8',
307
+ medium: 'w-12 h-12',
308
+ large: 'w-16 h-16',
309
+ xlarge: 'w-24 h-24'
310
+ };
311
+
312
+ return (
313
+ <div className={`skeleton-animated rounded-full ${sizeClasses[size]}`} />
314
+ );
315
+ }
316
+
317
+ /**
318
+ * SkeletonGrid - Grid of skeleton items
319
+ * Use for: Dashboard grids
320
+ */
321
+ export function SkeletonGrid({ columns = 3, rows = 2, Component = SkeletonCard }) {
322
+ const items = columns * rows;
323
+
324
+ return (
325
+ <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-${columns} gap-6`}>
326
+ {[...Array(items)].map((_, i) => (
327
+ <Component key={i} />
328
+ ))}
329
+ </div>
330
+ );
331
+ }
332
+
333
+ /**
334
+ * SkeletonBadge - Badge skeleton
335
+ * Use for: Status badges
336
+ */
337
+ export function SkeletonBadge() {
338
+ return (
339
+ <div className="skeleton-animated h-6 w-20 rounded-full inline-block" />
340
+ );
341
+ }
342
+
343
+ /**
344
+ * SkeletonButton - Button skeleton
345
+ * Use for: Button placeholders
346
+ */
347
+ export function SkeletonButton({ size = 'medium' }) {
348
+ const sizeClasses = {
349
+ small: 'h-8 w-20',
350
+ medium: 'h-10 w-24',
351
+ large: 'h-12 w-32'
352
+ };
353
+
354
+ return (
355
+ <div className={`skeleton-animated rounded-lg ${sizeClasses[size]}`} />
356
+ );
357
+ }
358
+
359
+ /**
360
+ * SkeletonDashboard - Complete dashboard skeleton
361
+ * Use for: Initial page load
362
+ */
363
+ export function SkeletonDashboard() {
364
+ return (
365
+ <div className="space-y-6">
366
+ {/* Stats overview */}
367
+ <SkeletonStatsOverview />
368
+
369
+ {/* Main grid */}
370
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
371
+ {/* Teams column */}
372
+ <div className="lg:col-span-2 space-y-6">
373
+ <SkeletonTeamCard />
374
+ <SkeletonTeamCard />
375
+ </div>
376
+
377
+ {/* Activity feed column */}
378
+ <div className="lg:col-span-1">
379
+ <SkeletonActivityFeed />
380
+ </div>
381
+ </div>
382
+ </div>
383
+ );
384
+ }