@agent-relay/dashboard 2.0.80 → 2.0.82

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.
Files changed (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
@@ -0,0 +1,488 @@
1
+ /**
2
+ * NotificationToast Component
3
+ *
4
+ * Toast notifications for alerts, messages, and system events.
5
+ * Supports multiple toast types and auto-dismiss.
6
+ */
7
+
8
+ import React, { useEffect, useState, useCallback } from 'react';
9
+ import { getAgentColor, getAgentInitials } from '../lib/colors';
10
+
11
+ export interface Toast {
12
+ id: string;
13
+ type: 'info' | 'success' | 'warning' | 'error' | 'message';
14
+ title: string;
15
+ message?: string;
16
+ agentName?: string;
17
+ duration?: number;
18
+ action?: {
19
+ label: string;
20
+ onClick: () => void;
21
+ };
22
+ }
23
+
24
+ export interface NotificationToastProps {
25
+ toasts: Toast[];
26
+ onDismiss: (id: string) => void;
27
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
28
+ maxVisible?: number;
29
+ }
30
+
31
+ export function NotificationToast({
32
+ toasts,
33
+ onDismiss,
34
+ position = 'top-right',
35
+ maxVisible = 5,
36
+ }: NotificationToastProps) {
37
+ const visibleToasts = toasts.slice(0, maxVisible);
38
+
39
+ return (
40
+ <div className={`toast-container toast-${position}`}>
41
+ {visibleToasts.map((toast) => (
42
+ <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />
43
+ ))}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ interface ToastItemProps {
49
+ toast: Toast;
50
+ onDismiss: (id: string) => void;
51
+ }
52
+
53
+ function ToastItem({ toast, onDismiss }: ToastItemProps) {
54
+ const [isExiting, setIsExiting] = useState(false);
55
+
56
+ const handleDismiss = useCallback(() => {
57
+ setIsExiting(true);
58
+ setTimeout(() => onDismiss(toast.id), 200);
59
+ }, [toast.id, onDismiss]);
60
+
61
+ // Auto-dismiss
62
+ useEffect(() => {
63
+ if (toast.duration === 0) return;
64
+
65
+ const duration = toast.duration || 5000;
66
+ const timer = setTimeout(handleDismiss, duration);
67
+ return () => clearTimeout(timer);
68
+ }, [toast.duration, handleDismiss]);
69
+
70
+ const colors = toast.agentName ? getAgentColor(toast.agentName) : null;
71
+ const Icon = getToastIcon(toast.type);
72
+
73
+ return (
74
+ <div
75
+ className={`toast toast-${toast.type} ${isExiting ? 'toast-exit' : ''}`}
76
+ role="alert"
77
+ >
78
+ <div className="toast-icon-wrapper">
79
+ {toast.agentName ? (
80
+ <div
81
+ className="toast-agent-avatar"
82
+ style={{ backgroundColor: colors?.primary, color: colors?.text }}
83
+ >
84
+ {getAgentInitials(toast.agentName)}
85
+ </div>
86
+ ) : (
87
+ <div className={`toast-icon toast-icon-${toast.type}`}>
88
+ <Icon />
89
+ </div>
90
+ )}
91
+ </div>
92
+
93
+ <div className="toast-content">
94
+ <div className="toast-header">
95
+ <span className="toast-title">{toast.title}</span>
96
+ {toast.agentName && <span className="toast-agent">@{toast.agentName}</span>}
97
+ </div>
98
+ {toast.message && <p className="toast-message">{toast.message}</p>}
99
+ {toast.action && (
100
+ <button
101
+ className="toast-action"
102
+ onClick={() => {
103
+ toast.action?.onClick();
104
+ handleDismiss();
105
+ }}
106
+ >
107
+ {toast.action.label}
108
+ </button>
109
+ )}
110
+ </div>
111
+
112
+ <button className="toast-close" onClick={handleDismiss} aria-label="Dismiss">
113
+ <CloseIcon />
114
+ </button>
115
+
116
+ {toast.duration !== 0 && (
117
+ <div
118
+ className="toast-progress"
119
+ style={{ animationDuration: `${toast.duration || 5000}ms` }}
120
+ />
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ // Hook for managing toasts
127
+ export function useToasts() {
128
+ const [toasts, setToasts] = useState<Toast[]>([]);
129
+
130
+ const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
131
+ const id = Math.random().toString(36).substring(2, 9);
132
+ setToasts((prev) => [...prev, { ...toast, id }]);
133
+ return id;
134
+ }, []);
135
+
136
+ const dismissToast = useCallback((id: string) => {
137
+ setToasts((prev) => prev.filter((t) => t.id !== id));
138
+ }, []);
139
+
140
+ const clearToasts = useCallback(() => {
141
+ setToasts([]);
142
+ }, []);
143
+
144
+ // Convenience methods
145
+ const info = useCallback(
146
+ (title: string, message?: string) => addToast({ type: 'info', title, message }),
147
+ [addToast]
148
+ );
149
+
150
+ const success = useCallback(
151
+ (title: string, message?: string) => addToast({ type: 'success', title, message }),
152
+ [addToast]
153
+ );
154
+
155
+ const warning = useCallback(
156
+ (title: string, message?: string) => addToast({ type: 'warning', title, message }),
157
+ [addToast]
158
+ );
159
+
160
+ const error = useCallback(
161
+ (title: string, message?: string) => addToast({ type: 'error', title, message }),
162
+ [addToast]
163
+ );
164
+
165
+ const message = useCallback(
166
+ (agentName: string, content: string, action?: Toast['action']) =>
167
+ addToast({ type: 'message', title: 'New Message', message: content, agentName, action }),
168
+ [addToast]
169
+ );
170
+
171
+ return {
172
+ toasts,
173
+ addToast,
174
+ dismissToast,
175
+ clearToasts,
176
+ info,
177
+ success,
178
+ warning,
179
+ error,
180
+ message,
181
+ };
182
+ }
183
+
184
+ // Helper function
185
+ function getToastIcon(type: Toast['type']) {
186
+ switch (type) {
187
+ case 'success':
188
+ return CheckIcon;
189
+ case 'warning':
190
+ return WarningIcon;
191
+ case 'error':
192
+ return ErrorIcon;
193
+ case 'message':
194
+ return MessageIcon;
195
+ default:
196
+ return InfoIcon;
197
+ }
198
+ }
199
+
200
+ // Icon components
201
+ function InfoIcon() {
202
+ return (
203
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
204
+ <circle cx="12" cy="12" r="10" />
205
+ <line x1="12" y1="16" x2="12" y2="12" />
206
+ <line x1="12" y1="8" x2="12.01" y2="8" />
207
+ </svg>
208
+ );
209
+ }
210
+
211
+ function CheckIcon() {
212
+ return (
213
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
214
+ <polyline points="20 6 9 17 4 12" />
215
+ </svg>
216
+ );
217
+ }
218
+
219
+ function WarningIcon() {
220
+ return (
221
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
222
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
223
+ <line x1="12" y1="9" x2="12" y2="13" />
224
+ <line x1="12" y1="17" x2="12.01" y2="17" />
225
+ </svg>
226
+ );
227
+ }
228
+
229
+ function ErrorIcon() {
230
+ return (
231
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
232
+ <circle cx="12" cy="12" r="10" />
233
+ <line x1="15" y1="9" x2="9" y2="15" />
234
+ <line x1="9" y1="9" x2="15" y2="15" />
235
+ </svg>
236
+ );
237
+ }
238
+
239
+ function MessageIcon() {
240
+ return (
241
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
242
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
243
+ </svg>
244
+ );
245
+ }
246
+
247
+ function CloseIcon() {
248
+ return (
249
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
250
+ <line x1="18" y1="6" x2="6" y2="18" />
251
+ <line x1="6" y1="6" x2="18" y2="18" />
252
+ </svg>
253
+ );
254
+ }
255
+
256
+ /**
257
+ * CSS styles for the notification toast
258
+ */
259
+ export const notificationToastStyles = `
260
+ .toast-container {
261
+ position: fixed;
262
+ z-index: 1100;
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 8px;
266
+ pointer-events: none;
267
+ }
268
+
269
+ .toast-top-right {
270
+ top: 16px;
271
+ right: 16px;
272
+ }
273
+
274
+ .toast-top-left {
275
+ top: 16px;
276
+ left: 16px;
277
+ }
278
+
279
+ .toast-bottom-right {
280
+ bottom: 16px;
281
+ right: 16px;
282
+ }
283
+
284
+ .toast-bottom-left {
285
+ bottom: 16px;
286
+ left: 16px;
287
+ }
288
+
289
+ .toast {
290
+ display: flex;
291
+ align-items: flex-start;
292
+ gap: 12px;
293
+ width: 360px;
294
+ padding: 14px 16px;
295
+ background: #ffffff;
296
+ border-radius: 8px;
297
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
298
+ pointer-events: auto;
299
+ animation: toastEnter 0.2s ease;
300
+ position: relative;
301
+ overflow: hidden;
302
+ }
303
+
304
+ .toast-exit {
305
+ animation: toastExit 0.2s ease forwards;
306
+ }
307
+
308
+ .toast-icon-wrapper {
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .toast-icon {
313
+ width: 32px;
314
+ height: 32px;
315
+ border-radius: 50%;
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ }
320
+
321
+ .toast-icon-info {
322
+ background: #dbeafe;
323
+ color: #2563eb;
324
+ }
325
+
326
+ .toast-icon-success {
327
+ background: #dcfce7;
328
+ color: #16a34a;
329
+ }
330
+
331
+ .toast-icon-warning {
332
+ background: #fef3c7;
333
+ color: #d97706;
334
+ }
335
+
336
+ .toast-icon-error {
337
+ background: #fee2e2;
338
+ color: #dc2626;
339
+ }
340
+
341
+ .toast-icon-message {
342
+ background: #f3e8ff;
343
+ color: #7c3aed;
344
+ }
345
+
346
+ .toast-agent-avatar {
347
+ width: 32px;
348
+ height: 32px;
349
+ border-radius: 8px;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ font-size: 11px;
354
+ font-weight: 600;
355
+ }
356
+
357
+ .toast-content {
358
+ flex: 1;
359
+ min-width: 0;
360
+ }
361
+
362
+ .toast-header {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 8px;
366
+ }
367
+
368
+ .toast-title {
369
+ font-size: 14px;
370
+ font-weight: 600;
371
+ color: #1a1a1a;
372
+ }
373
+
374
+ .toast-agent {
375
+ font-size: 12px;
376
+ color: #888;
377
+ }
378
+
379
+ .toast-message {
380
+ margin: 4px 0 0;
381
+ font-size: 13px;
382
+ color: #555;
383
+ line-height: 1.4;
384
+ display: -webkit-box;
385
+ -webkit-line-clamp: 2;
386
+ -webkit-box-orient: vertical;
387
+ overflow: hidden;
388
+ }
389
+
390
+ .toast-action {
391
+ margin-top: 8px;
392
+ padding: 6px 12px;
393
+ background: #f5f5f5;
394
+ border: none;
395
+ border-radius: 4px;
396
+ font-size: 12px;
397
+ font-weight: 500;
398
+ color: #333;
399
+ cursor: pointer;
400
+ font-family: inherit;
401
+ transition: background 0.15s;
402
+ }
403
+
404
+ .toast-action:hover {
405
+ background: #e8e8e8;
406
+ }
407
+
408
+ .toast-close {
409
+ flex-shrink: 0;
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ width: 24px;
414
+ height: 24px;
415
+ background: transparent;
416
+ border: none;
417
+ border-radius: 4px;
418
+ color: #888;
419
+ cursor: pointer;
420
+ transition: all 0.15s;
421
+ }
422
+
423
+ .toast-close:hover {
424
+ background: #f5f5f5;
425
+ color: #333;
426
+ }
427
+
428
+ .toast-progress {
429
+ position: absolute;
430
+ bottom: 0;
431
+ left: 0;
432
+ height: 3px;
433
+ background: currentColor;
434
+ opacity: 0.3;
435
+ animation: toastProgress linear forwards;
436
+ }
437
+
438
+ .toast-info .toast-progress {
439
+ color: #2563eb;
440
+ }
441
+
442
+ .toast-success .toast-progress {
443
+ color: #16a34a;
444
+ }
445
+
446
+ .toast-warning .toast-progress {
447
+ color: #d97706;
448
+ }
449
+
450
+ .toast-error .toast-progress {
451
+ color: #dc2626;
452
+ }
453
+
454
+ .toast-message .toast-progress {
455
+ color: #7c3aed;
456
+ }
457
+
458
+ @keyframes toastEnter {
459
+ from {
460
+ opacity: 0;
461
+ transform: translateX(100%);
462
+ }
463
+ to {
464
+ opacity: 1;
465
+ transform: translateX(0);
466
+ }
467
+ }
468
+
469
+ @keyframes toastExit {
470
+ from {
471
+ opacity: 1;
472
+ transform: translateX(0);
473
+ }
474
+ to {
475
+ opacity: 0;
476
+ transform: translateX(100%);
477
+ }
478
+ }
479
+
480
+ @keyframes toastProgress {
481
+ from {
482
+ width: 100%;
483
+ }
484
+ to {
485
+ width: 0%;
486
+ }
487
+ }
488
+ `;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * OnlineUsersIndicator Component
3
+ *
4
+ * Shows a row of avatars for online users with a count indicator.
5
+ * Clicking reveals a dropdown list with all online users.
6
+ */
7
+
8
+ import React, { useState, useRef, useEffect } from 'react';
9
+ import type { UserPresence } from './hooks/usePresence';
10
+
11
+ export interface OnlineUsersIndicatorProps {
12
+ /** List of online users */
13
+ onlineUsers: UserPresence[];
14
+ /** Callback when a user is clicked (for profile viewing) */
15
+ onUserClick?: (user: UserPresence) => void;
16
+ /** Maximum avatars to show before "+N" */
17
+ maxAvatars?: number;
18
+ }
19
+
20
+ export function OnlineUsersIndicator({
21
+ onlineUsers,
22
+ onUserClick,
23
+ maxAvatars = 4,
24
+ }: OnlineUsersIndicatorProps) {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const dropdownRef = useRef<HTMLDivElement>(null);
27
+
28
+ // Close dropdown when clicking outside
29
+ useEffect(() => {
30
+ const handleClickOutside = (event: MouseEvent) => {
31
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
32
+ setIsOpen(false);
33
+ }
34
+ };
35
+
36
+ if (isOpen) {
37
+ document.addEventListener('mousedown', handleClickOutside);
38
+ }
39
+ return () => document.removeEventListener('mousedown', handleClickOutside);
40
+ }, [isOpen]);
41
+
42
+ if (onlineUsers.length === 0) {
43
+ return null;
44
+ }
45
+
46
+ const displayedUsers = onlineUsers.slice(0, maxAvatars);
47
+ const remainingCount = Math.max(0, onlineUsers.length - maxAvatars);
48
+
49
+ return (
50
+ <div className="relative" ref={dropdownRef}>
51
+ {/* Compact avatar row */}
52
+ <button
53
+ onClick={() => setIsOpen(!isOpen)}
54
+ className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-white/[0.05] transition-colors"
55
+ title={`${onlineUsers.length} user${onlineUsers.length !== 1 ? 's' : ''} online`}
56
+ >
57
+ {/* Green online indicator */}
58
+ <div className="w-2 h-2 bg-green-500 rounded-full" />
59
+
60
+ {/* Stacked avatars */}
61
+ <div className="flex -space-x-1.5">
62
+ {displayedUsers.map((user) => (
63
+ <div
64
+ key={user.username}
65
+ className="relative ring-2 ring-[#1a1d21] rounded-full"
66
+ title={user.username}
67
+ >
68
+ {user.avatarUrl ? (
69
+ <img
70
+ src={user.avatarUrl}
71
+ alt={user.username}
72
+ className="w-6 h-6 rounded-full object-cover"
73
+ />
74
+ ) : (
75
+ <div className="w-6 h-6 rounded-full bg-[#a855f7] flex items-center justify-center text-[10px] text-white font-medium">
76
+ {user.username.charAt(0).toUpperCase()}
77
+ </div>
78
+ )}
79
+ </div>
80
+ ))}
81
+ {remainingCount > 0 && (
82
+ <div className="w-6 h-6 rounded-full bg-[#3d4043] ring-2 ring-[#1a1d21] flex items-center justify-center text-[10px] text-[#d1d2d3] font-medium">
83
+ +{remainingCount}
84
+ </div>
85
+ )}
86
+ </div>
87
+
88
+ {/* Count text */}
89
+ <span className="text-xs text-[#8d8d8e]">
90
+ {onlineUsers.length} online
91
+ </span>
92
+ </button>
93
+
94
+ {/* Dropdown list */}
95
+ {isOpen && (
96
+ <div className="absolute right-0 top-full mt-1 w-64 bg-[#1a1d21] border border-white/10 rounded-lg shadow-xl z-50 max-h-[300px] overflow-y-auto">
97
+ <div className="p-2 border-b border-white/10">
98
+ <h3 className="text-sm font-medium text-[#d1d2d3]">Online Users</h3>
99
+ </div>
100
+ <div className="py-1">
101
+ {onlineUsers.map((user) => (
102
+ <button
103
+ key={user.username}
104
+ onClick={() => {
105
+ onUserClick?.(user);
106
+ setIsOpen(false);
107
+ }}
108
+ className="w-full flex items-center gap-3 px-3 py-2 hover:bg-white/[0.05] transition-colors text-left"
109
+ >
110
+ {/* Avatar with online indicator */}
111
+ <div className="relative">
112
+ {user.avatarUrl ? (
113
+ <img
114
+ src={user.avatarUrl}
115
+ alt={user.username}
116
+ className="w-8 h-8 rounded-full object-cover"
117
+ />
118
+ ) : (
119
+ <div className="w-8 h-8 rounded-full bg-[#a855f7] flex items-center justify-center text-xs text-white font-medium">
120
+ {user.username.charAt(0).toUpperCase()}
121
+ </div>
122
+ )}
123
+ {/* Green online dot */}
124
+ <div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 rounded-full border-2 border-[#1a1d21]" />
125
+ </div>
126
+
127
+ {/* User info */}
128
+ <div className="flex-1 min-w-0">
129
+ <div className="text-sm font-medium text-[#d1d2d3] truncate">
130
+ {user.username}
131
+ </div>
132
+ <div className="text-xs text-[#8d8d8e]">
133
+ Online since {formatTime(user.connectedAt)}
134
+ </div>
135
+ </div>
136
+ </button>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Format a timestamp to a readable time
147
+ */
148
+ function formatTime(timestamp: string): string {
149
+ const date = new Date(timestamp);
150
+ const now = new Date();
151
+
152
+ // If same day, show time only
153
+ if (date.toDateString() === now.toDateString()) {
154
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
155
+ }
156
+
157
+ // Otherwise show date and time
158
+ return date.toLocaleDateString([], {
159
+ month: 'short',
160
+ day: 'numeric',
161
+ hour: '2-digit',
162
+ minute: '2-digit',
163
+ });
164
+ }