@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.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelAdminPanel Component
|
|
3
|
+
*
|
|
4
|
+
* Admin panel for managing channel settings, members, and agent assignments.
|
|
5
|
+
* Slide-over panel with tabs for Settings and Members.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
9
|
+
import { useChannelAdmin, type ChannelMemberInfo } from './hooks/useChannelAdmin';
|
|
10
|
+
import { Pagination } from './Pagination';
|
|
11
|
+
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
12
|
+
|
|
13
|
+
export interface ChannelAdminPanelProps {
|
|
14
|
+
channelId: string;
|
|
15
|
+
channelName: string;
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
currentUserId?: string;
|
|
19
|
+
/** List of available agents for assignment */
|
|
20
|
+
availableAgents?: Array<{ name: string; status: string }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TabId = 'settings' | 'members';
|
|
24
|
+
|
|
25
|
+
export function ChannelAdminPanel({
|
|
26
|
+
channelId,
|
|
27
|
+
channelName,
|
|
28
|
+
isOpen,
|
|
29
|
+
onClose,
|
|
30
|
+
currentUserId,
|
|
31
|
+
availableAgents = [],
|
|
32
|
+
}: ChannelAdminPanelProps) {
|
|
33
|
+
const [activeTab, setActiveTab] = useState<TabId>('settings');
|
|
34
|
+
const [editDescription, setEditDescription] = useState('');
|
|
35
|
+
const [editTopic, setEditTopic] = useState('');
|
|
36
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
37
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
// Confirmation dialog state
|
|
40
|
+
const [removeMemberId, setRemoveMemberId] = useState<string | null>(null);
|
|
41
|
+
const [removeMemberName, setRemoveMemberName] = useState<string>('');
|
|
42
|
+
const [isRemoving, setIsRemoving] = useState(false);
|
|
43
|
+
|
|
44
|
+
// Agent assignment state
|
|
45
|
+
const [showAgentSelector, setShowAgentSelector] = useState(false);
|
|
46
|
+
const [assigningAgent, setAssigningAgent] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
settings,
|
|
50
|
+
members,
|
|
51
|
+
isLoadingSettings,
|
|
52
|
+
isLoadingMembers,
|
|
53
|
+
settingsError,
|
|
54
|
+
membersError,
|
|
55
|
+
isAdmin,
|
|
56
|
+
memberPage,
|
|
57
|
+
memberTotalPages,
|
|
58
|
+
memberTotalCount,
|
|
59
|
+
goToMemberPage,
|
|
60
|
+
memberSearchQuery,
|
|
61
|
+
setMemberSearchQuery,
|
|
62
|
+
updateSettings,
|
|
63
|
+
removeMember,
|
|
64
|
+
assignAgent,
|
|
65
|
+
setMemberRole,
|
|
66
|
+
refreshSettings,
|
|
67
|
+
refreshMembers,
|
|
68
|
+
} = useChannelAdmin({
|
|
69
|
+
channelId,
|
|
70
|
+
currentUserId,
|
|
71
|
+
autoFetch: isOpen,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Initialize edit fields when settings load
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (settings) {
|
|
77
|
+
setEditDescription(settings.description || '');
|
|
78
|
+
setEditTopic(settings.topic || '');
|
|
79
|
+
}
|
|
80
|
+
}, [settings]);
|
|
81
|
+
|
|
82
|
+
// Handle save settings
|
|
83
|
+
const handleSaveSettings = useCallback(async () => {
|
|
84
|
+
if (!isAdmin) return;
|
|
85
|
+
|
|
86
|
+
setIsSaving(true);
|
|
87
|
+
setSaveError(null);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const updates: { description?: string; topic?: string } = {};
|
|
91
|
+
|
|
92
|
+
if (editDescription !== (settings?.description || '')) {
|
|
93
|
+
updates.description = editDescription.trim() || undefined;
|
|
94
|
+
}
|
|
95
|
+
if (editTopic !== (settings?.topic || '')) {
|
|
96
|
+
updates.topic = editTopic.trim() || undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Object.keys(updates).length > 0) {
|
|
100
|
+
await updateSettings(updates);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
|
104
|
+
} finally {
|
|
105
|
+
setIsSaving(false);
|
|
106
|
+
}
|
|
107
|
+
}, [isAdmin, editDescription, editTopic, settings, updateSettings]);
|
|
108
|
+
|
|
109
|
+
// Handle remove member confirmation
|
|
110
|
+
const handleRemoveMember = useCallback((member: ChannelMemberInfo) => {
|
|
111
|
+
setRemoveMemberId(member.id);
|
|
112
|
+
setRemoveMemberName(member.displayName || member.name);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// Confirm remove member
|
|
116
|
+
const confirmRemoveMember = useCallback(async () => {
|
|
117
|
+
if (!removeMemberId) return;
|
|
118
|
+
|
|
119
|
+
setIsRemoving(true);
|
|
120
|
+
try {
|
|
121
|
+
await removeMember(removeMemberId);
|
|
122
|
+
setRemoveMemberId(null);
|
|
123
|
+
setRemoveMemberName('');
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error('Failed to remove member:', err);
|
|
126
|
+
} finally {
|
|
127
|
+
setIsRemoving(false);
|
|
128
|
+
}
|
|
129
|
+
}, [removeMemberId, removeMember]);
|
|
130
|
+
|
|
131
|
+
// Handle agent assignment
|
|
132
|
+
const handleAssignAgent = useCallback(async (agentName: string) => {
|
|
133
|
+
setAssigningAgent(agentName);
|
|
134
|
+
try {
|
|
135
|
+
await assignAgent(agentName);
|
|
136
|
+
setShowAgentSelector(false);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error('Failed to assign agent:', err);
|
|
139
|
+
} finally {
|
|
140
|
+
setAssigningAgent(null);
|
|
141
|
+
}
|
|
142
|
+
}, [assignAgent]);
|
|
143
|
+
|
|
144
|
+
// Filter available agents (not already members)
|
|
145
|
+
const unassignedAgents = useMemo(() => {
|
|
146
|
+
const memberNames = new Set(members.filter((m) => m.isAgent).map((m) => m.name.toLowerCase()));
|
|
147
|
+
return availableAgents.filter((a) => !memberNames.has(a.name.toLowerCase()));
|
|
148
|
+
}, [availableAgents, members]);
|
|
149
|
+
|
|
150
|
+
// Has unsaved changes
|
|
151
|
+
const hasChanges = useMemo(() => {
|
|
152
|
+
if (!settings) return false;
|
|
153
|
+
return (
|
|
154
|
+
editDescription !== (settings.description || '') ||
|
|
155
|
+
editTopic !== (settings.topic || '')
|
|
156
|
+
);
|
|
157
|
+
}, [settings, editDescription, editTopic]);
|
|
158
|
+
|
|
159
|
+
if (!isOpen) return null;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<div
|
|
164
|
+
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
|
165
|
+
onClick={onClose}
|
|
166
|
+
/>
|
|
167
|
+
<div
|
|
168
|
+
className="fixed right-0 top-0 bottom-0 w-full max-w-lg bg-bg-primary border-l border-border-subtle shadow-2xl z-50 flex flex-col animate-in slide-in-from-right duration-300"
|
|
169
|
+
onClick={(e) => e.stopPropagation()}
|
|
170
|
+
>
|
|
171
|
+
{/* Header */}
|
|
172
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border-subtle bg-bg-secondary">
|
|
173
|
+
<div className="flex items-center gap-3">
|
|
174
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-accent-cyan/20 to-blue-500/20 flex items-center justify-center border border-accent-cyan/30">
|
|
175
|
+
<SettingsIcon />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<h2 className="text-lg font-semibold text-text-primary m-0">
|
|
179
|
+
#{channelName}
|
|
180
|
+
</h2>
|
|
181
|
+
<p className="text-xs text-text-muted m-0">
|
|
182
|
+
{isAdmin ? 'Channel Settings' : 'Channel Info'}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<button
|
|
187
|
+
onClick={onClose}
|
|
188
|
+
className="w-10 h-10 rounded-lg bg-bg-tertiary border border-border-subtle flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all"
|
|
189
|
+
title="Close"
|
|
190
|
+
>
|
|
191
|
+
<CloseIcon />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Tabs */}
|
|
196
|
+
<div className="flex border-b border-border-subtle">
|
|
197
|
+
<TabButton
|
|
198
|
+
label="Settings"
|
|
199
|
+
isActive={activeTab === 'settings'}
|
|
200
|
+
onClick={() => setActiveTab('settings')}
|
|
201
|
+
/>
|
|
202
|
+
<TabButton
|
|
203
|
+
label={`Members (${memberTotalCount})`}
|
|
204
|
+
isActive={activeTab === 'members'}
|
|
205
|
+
onClick={() => setActiveTab('members')}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Content */}
|
|
210
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
211
|
+
{activeTab === 'settings' ? (
|
|
212
|
+
<SettingsTab
|
|
213
|
+
settings={settings}
|
|
214
|
+
isLoading={isLoadingSettings}
|
|
215
|
+
error={settingsError}
|
|
216
|
+
isAdmin={isAdmin}
|
|
217
|
+
editDescription={editDescription}
|
|
218
|
+
setEditDescription={setEditDescription}
|
|
219
|
+
editTopic={editTopic}
|
|
220
|
+
setEditTopic={setEditTopic}
|
|
221
|
+
hasChanges={hasChanges}
|
|
222
|
+
isSaving={isSaving}
|
|
223
|
+
saveError={saveError}
|
|
224
|
+
onSave={handleSaveSettings}
|
|
225
|
+
onRefresh={refreshSettings}
|
|
226
|
+
/>
|
|
227
|
+
) : (
|
|
228
|
+
<MembersTab
|
|
229
|
+
members={members}
|
|
230
|
+
isLoading={isLoadingMembers}
|
|
231
|
+
error={membersError}
|
|
232
|
+
isAdmin={isAdmin}
|
|
233
|
+
currentUserId={currentUserId}
|
|
234
|
+
searchQuery={memberSearchQuery}
|
|
235
|
+
setSearchQuery={setMemberSearchQuery}
|
|
236
|
+
currentPage={memberPage}
|
|
237
|
+
totalPages={memberTotalPages}
|
|
238
|
+
onPageChange={goToMemberPage}
|
|
239
|
+
onRemoveMember={handleRemoveMember}
|
|
240
|
+
onToggleRole={setMemberRole}
|
|
241
|
+
showAgentSelector={showAgentSelector}
|
|
242
|
+
setShowAgentSelector={setShowAgentSelector}
|
|
243
|
+
unassignedAgents={unassignedAgents}
|
|
244
|
+
assigningAgent={assigningAgent}
|
|
245
|
+
onAssignAgent={handleAssignAgent}
|
|
246
|
+
onRefresh={refreshMembers}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Remove Member Confirmation */}
|
|
253
|
+
<ConfirmationDialog
|
|
254
|
+
isOpen={removeMemberId !== null}
|
|
255
|
+
title="Remove Member"
|
|
256
|
+
message={`Are you sure you want to remove "${removeMemberName}" from #${channelName}? They will need to rejoin to access this channel.`}
|
|
257
|
+
confirmLabel="Remove"
|
|
258
|
+
confirmVariant="danger"
|
|
259
|
+
onConfirm={confirmRemoveMember}
|
|
260
|
+
onCancel={() => {
|
|
261
|
+
setRemoveMemberId(null);
|
|
262
|
+
setRemoveMemberName('');
|
|
263
|
+
}}
|
|
264
|
+
isProcessing={isRemoving}
|
|
265
|
+
/>
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Tab Button
|
|
271
|
+
interface TabButtonProps {
|
|
272
|
+
label: string;
|
|
273
|
+
isActive: boolean;
|
|
274
|
+
onClick: () => void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function TabButton({ label, isActive, onClick }: TabButtonProps) {
|
|
278
|
+
return (
|
|
279
|
+
<button
|
|
280
|
+
onClick={onClick}
|
|
281
|
+
className={`
|
|
282
|
+
flex-1 px-4 py-3 text-sm font-medium transition-colors relative
|
|
283
|
+
${isActive
|
|
284
|
+
? 'text-accent-cyan'
|
|
285
|
+
: 'text-text-muted hover:text-text-primary'
|
|
286
|
+
}
|
|
287
|
+
`}
|
|
288
|
+
>
|
|
289
|
+
{label}
|
|
290
|
+
{isActive && (
|
|
291
|
+
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent-cyan" />
|
|
292
|
+
)}
|
|
293
|
+
</button>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Settings Tab
|
|
298
|
+
interface SettingsTabProps {
|
|
299
|
+
settings: ReturnType<typeof useChannelAdmin>['settings'];
|
|
300
|
+
isLoading: boolean;
|
|
301
|
+
error: string | null;
|
|
302
|
+
isAdmin: boolean;
|
|
303
|
+
editDescription: string;
|
|
304
|
+
setEditDescription: (value: string) => void;
|
|
305
|
+
editTopic: string;
|
|
306
|
+
setEditTopic: (value: string) => void;
|
|
307
|
+
hasChanges: boolean;
|
|
308
|
+
isSaving: boolean;
|
|
309
|
+
saveError: string | null;
|
|
310
|
+
onSave: () => void;
|
|
311
|
+
onRefresh: () => void;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function SettingsTab({
|
|
315
|
+
settings,
|
|
316
|
+
isLoading,
|
|
317
|
+
error,
|
|
318
|
+
isAdmin,
|
|
319
|
+
editDescription,
|
|
320
|
+
setEditDescription,
|
|
321
|
+
editTopic,
|
|
322
|
+
setEditTopic,
|
|
323
|
+
hasChanges,
|
|
324
|
+
isSaving,
|
|
325
|
+
saveError,
|
|
326
|
+
onSave,
|
|
327
|
+
onRefresh,
|
|
328
|
+
}: SettingsTabProps) {
|
|
329
|
+
if (isLoading) {
|
|
330
|
+
return (
|
|
331
|
+
<div className="py-12 text-center">
|
|
332
|
+
<LoadingSpinner />
|
|
333
|
+
<p className="text-sm text-text-muted mt-2">Loading settings...</p>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (error) {
|
|
339
|
+
return (
|
|
340
|
+
<div className="py-12 text-center">
|
|
341
|
+
<ErrorIcon />
|
|
342
|
+
<p className="text-sm text-error mt-2">{error}</p>
|
|
343
|
+
<button onClick={onRefresh} className="mt-3 text-sm text-accent-cyan hover:underline">
|
|
344
|
+
Try again
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!settings) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className="space-y-6">
|
|
356
|
+
{/* Description */}
|
|
357
|
+
<div>
|
|
358
|
+
<label className="block text-sm font-medium text-text-secondary mb-1.5">
|
|
359
|
+
Description
|
|
360
|
+
</label>
|
|
361
|
+
<textarea
|
|
362
|
+
value={editDescription}
|
|
363
|
+
onChange={(e) => setEditDescription(e.target.value)}
|
|
364
|
+
placeholder="What's this channel about?"
|
|
365
|
+
rows={3}
|
|
366
|
+
disabled={!isAdmin}
|
|
367
|
+
className="w-full px-4 py-2.5 bg-bg-tertiary border border-sidebar-border rounded-lg text-sm text-text-primary placeholder:text-text-muted outline-none focus:border-accent-cyan/50 transition-colors resize-none disabled:opacity-60 disabled:cursor-not-allowed"
|
|
368
|
+
maxLength={200}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{/* Topic */}
|
|
373
|
+
<div>
|
|
374
|
+
<label className="block text-sm font-medium text-text-secondary mb-1.5">
|
|
375
|
+
Topic
|
|
376
|
+
</label>
|
|
377
|
+
<input
|
|
378
|
+
type="text"
|
|
379
|
+
value={editTopic}
|
|
380
|
+
onChange={(e) => setEditTopic(e.target.value)}
|
|
381
|
+
placeholder="Current topic of discussion"
|
|
382
|
+
disabled={!isAdmin}
|
|
383
|
+
className="w-full px-4 py-2.5 bg-bg-tertiary border border-sidebar-border rounded-lg text-sm text-text-primary placeholder:text-text-muted outline-none focus:border-accent-cyan/50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
384
|
+
maxLength={100}
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{/* Channel Info */}
|
|
389
|
+
<div className="pt-4 border-t border-border-subtle">
|
|
390
|
+
<h4 className="text-sm font-medium text-text-secondary mb-3">Channel Info</h4>
|
|
391
|
+
<div className="space-y-2 text-sm">
|
|
392
|
+
<div className="flex justify-between">
|
|
393
|
+
<span className="text-text-muted">Created</span>
|
|
394
|
+
<span className="text-text-primary">
|
|
395
|
+
{new Date(settings.createdAt).toLocaleDateString()}
|
|
396
|
+
</span>
|
|
397
|
+
</div>
|
|
398
|
+
<div className="flex justify-between">
|
|
399
|
+
<span className="text-text-muted">Visibility</span>
|
|
400
|
+
<span className={`${settings.isPrivate ? 'text-warning' : 'text-accent-cyan'}`}>
|
|
401
|
+
{settings.isPrivate ? 'Private' : 'Public'}
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div className="flex justify-between">
|
|
405
|
+
<span className="text-text-muted">Admins</span>
|
|
406
|
+
<span className="text-text-primary">
|
|
407
|
+
{settings.admins.length}
|
|
408
|
+
</span>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{/* Save Button */}
|
|
414
|
+
{isAdmin && (
|
|
415
|
+
<div className="pt-4">
|
|
416
|
+
{saveError && (
|
|
417
|
+
<div className="mb-3 p-3 bg-error/10 border border-error/20 rounded-lg flex items-center gap-2">
|
|
418
|
+
<ErrorIcon className="w-4 h-4" />
|
|
419
|
+
<p className="text-sm text-error">{saveError}</p>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
<button
|
|
423
|
+
onClick={onSave}
|
|
424
|
+
disabled={!hasChanges || isSaving}
|
|
425
|
+
className="w-full px-4 py-2.5 text-sm font-medium bg-accent-cyan text-bg-deep rounded-lg hover:bg-accent-cyan/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
|
|
426
|
+
>
|
|
427
|
+
{isSaving && <MiniSpinner />}
|
|
428
|
+
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
433
|
+
{!isAdmin && (
|
|
434
|
+
<div className="p-3 bg-warning/10 border border-warning/20 rounded-lg">
|
|
435
|
+
<p className="text-sm text-warning">
|
|
436
|
+
You need admin permissions to edit channel settings.
|
|
437
|
+
</p>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Members Tab
|
|
445
|
+
interface MembersTabProps {
|
|
446
|
+
members: ChannelMemberInfo[];
|
|
447
|
+
isLoading: boolean;
|
|
448
|
+
error: string | null;
|
|
449
|
+
isAdmin: boolean;
|
|
450
|
+
currentUserId?: string;
|
|
451
|
+
searchQuery: string;
|
|
452
|
+
setSearchQuery: (query: string) => void;
|
|
453
|
+
currentPage: number;
|
|
454
|
+
totalPages: number;
|
|
455
|
+
onPageChange: (page: number) => void;
|
|
456
|
+
onRemoveMember: (member: ChannelMemberInfo) => void;
|
|
457
|
+
onToggleRole: (memberId: string, role: 'admin' | 'member') => Promise<void>;
|
|
458
|
+
showAgentSelector: boolean;
|
|
459
|
+
setShowAgentSelector: (show: boolean) => void;
|
|
460
|
+
unassignedAgents: Array<{ name: string; status: string }>;
|
|
461
|
+
assigningAgent: string | null;
|
|
462
|
+
onAssignAgent: (agentName: string) => void;
|
|
463
|
+
onRefresh: () => void;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function MembersTab({
|
|
467
|
+
members,
|
|
468
|
+
isLoading,
|
|
469
|
+
error,
|
|
470
|
+
isAdmin,
|
|
471
|
+
currentUserId,
|
|
472
|
+
searchQuery,
|
|
473
|
+
setSearchQuery,
|
|
474
|
+
currentPage,
|
|
475
|
+
totalPages,
|
|
476
|
+
onPageChange,
|
|
477
|
+
onRemoveMember,
|
|
478
|
+
onToggleRole,
|
|
479
|
+
showAgentSelector,
|
|
480
|
+
setShowAgentSelector,
|
|
481
|
+
unassignedAgents,
|
|
482
|
+
assigningAgent,
|
|
483
|
+
onAssignAgent,
|
|
484
|
+
onRefresh,
|
|
485
|
+
}: MembersTabProps) {
|
|
486
|
+
return (
|
|
487
|
+
<div className="space-y-4">
|
|
488
|
+
{/* Search and Actions */}
|
|
489
|
+
<div className="flex gap-2">
|
|
490
|
+
<div className="flex-1 relative">
|
|
491
|
+
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
492
|
+
<input
|
|
493
|
+
type="text"
|
|
494
|
+
value={searchQuery}
|
|
495
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
496
|
+
placeholder="Search members..."
|
|
497
|
+
className="w-full pl-10 pr-4 py-2 bg-bg-tertiary border border-sidebar-border rounded-lg text-sm text-text-primary placeholder:text-text-muted outline-none focus:border-accent-cyan/50 transition-colors"
|
|
498
|
+
/>
|
|
499
|
+
</div>
|
|
500
|
+
{isAdmin && (
|
|
501
|
+
<button
|
|
502
|
+
onClick={() => setShowAgentSelector(!showAgentSelector)}
|
|
503
|
+
className="px-3 py-2 bg-accent-cyan text-bg-deep text-sm font-medium rounded-lg hover:bg-accent-cyan/90 transition-colors flex items-center gap-1"
|
|
504
|
+
>
|
|
505
|
+
<PlusIcon className="w-4 h-4" />
|
|
506
|
+
Add Agent
|
|
507
|
+
</button>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
{/* Agent Selector */}
|
|
512
|
+
{showAgentSelector && isAdmin && (
|
|
513
|
+
<div className="p-3 bg-bg-tertiary rounded-lg border border-sidebar-border">
|
|
514
|
+
<p className="text-xs text-text-muted mb-2">Select an agent to add:</p>
|
|
515
|
+
{unassignedAgents.length === 0 ? (
|
|
516
|
+
<p className="text-sm text-text-muted">All agents are already members</p>
|
|
517
|
+
) : (
|
|
518
|
+
<div className="flex flex-wrap gap-2">
|
|
519
|
+
{unassignedAgents.map((agent) => (
|
|
520
|
+
<button
|
|
521
|
+
key={agent.name}
|
|
522
|
+
onClick={() => onAssignAgent(agent.name)}
|
|
523
|
+
disabled={assigningAgent !== null}
|
|
524
|
+
className="px-3 py-1.5 text-xs font-medium bg-bg-secondary border border-sidebar-border rounded-md hover:border-accent-cyan/50 disabled:opacity-50 transition-colors flex items-center gap-1"
|
|
525
|
+
>
|
|
526
|
+
{assigningAgent === agent.name ? (
|
|
527
|
+
<MiniSpinner />
|
|
528
|
+
) : (
|
|
529
|
+
<span className={`w-2 h-2 rounded-full ${
|
|
530
|
+
agent.status === 'online' ? 'bg-success' : 'bg-text-muted'
|
|
531
|
+
}`} />
|
|
532
|
+
)}
|
|
533
|
+
{agent.name}
|
|
534
|
+
</button>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{/* Members List */}
|
|
542
|
+
{isLoading && members.length === 0 ? (
|
|
543
|
+
<div className="py-12 text-center">
|
|
544
|
+
<LoadingSpinner />
|
|
545
|
+
<p className="text-sm text-text-muted mt-2">Loading members...</p>
|
|
546
|
+
</div>
|
|
547
|
+
) : error ? (
|
|
548
|
+
<div className="py-12 text-center">
|
|
549
|
+
<ErrorIcon />
|
|
550
|
+
<p className="text-sm text-error mt-2">{error}</p>
|
|
551
|
+
<button onClick={onRefresh} className="mt-3 text-sm text-accent-cyan hover:underline">
|
|
552
|
+
Try again
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
) : members.length === 0 ? (
|
|
556
|
+
<div className="py-12 text-center">
|
|
557
|
+
<p className="text-sm text-text-muted">
|
|
558
|
+
{searchQuery ? 'No members found' : 'No members yet'}
|
|
559
|
+
</p>
|
|
560
|
+
</div>
|
|
561
|
+
) : (
|
|
562
|
+
<div className="space-y-1">
|
|
563
|
+
{members.map((member) => (
|
|
564
|
+
<MemberRow
|
|
565
|
+
key={member.id}
|
|
566
|
+
member={member}
|
|
567
|
+
isAdmin={isAdmin}
|
|
568
|
+
isSelf={member.id === currentUserId}
|
|
569
|
+
onRemove={() => onRemoveMember(member)}
|
|
570
|
+
onToggleRole={() => onToggleRole(member.id, member.role === 'admin' ? 'member' : 'admin')}
|
|
571
|
+
/>
|
|
572
|
+
))}
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
|
|
576
|
+
{/* Pagination */}
|
|
577
|
+
{totalPages > 1 && (
|
|
578
|
+
<div className="pt-4">
|
|
579
|
+
<Pagination
|
|
580
|
+
currentPage={currentPage}
|
|
581
|
+
totalPages={totalPages}
|
|
582
|
+
onPageChange={onPageChange}
|
|
583
|
+
/>
|
|
584
|
+
</div>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Member Row
|
|
591
|
+
interface MemberRowProps {
|
|
592
|
+
member: ChannelMemberInfo;
|
|
593
|
+
isAdmin: boolean;
|
|
594
|
+
isSelf: boolean;
|
|
595
|
+
onRemove: () => void;
|
|
596
|
+
onToggleRole: () => void;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function MemberRow({ member, isAdmin, isSelf, onRemove, onToggleRole }: MemberRowProps) {
|
|
600
|
+
const [showMenu, setShowMenu] = useState(false);
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<div className="flex items-center gap-3 p-3 rounded-lg hover:bg-sidebar-border/50 transition-colors group">
|
|
604
|
+
{/* Avatar */}
|
|
605
|
+
<div className="w-9 h-9 rounded-full bg-accent-cyan/20 flex items-center justify-center flex-shrink-0">
|
|
606
|
+
{member.avatarUrl ? (
|
|
607
|
+
<img
|
|
608
|
+
src={member.avatarUrl}
|
|
609
|
+
alt={member.displayName || member.name}
|
|
610
|
+
className="w-9 h-9 rounded-full object-cover"
|
|
611
|
+
/>
|
|
612
|
+
) : (
|
|
613
|
+
<span className="text-sm font-medium text-accent-cyan">
|
|
614
|
+
{(member.displayName || member.name).charAt(0).toUpperCase()}
|
|
615
|
+
</span>
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
{/* Info */}
|
|
620
|
+
<div className="flex-1 min-w-0">
|
|
621
|
+
<div className="flex items-center gap-2">
|
|
622
|
+
<span className="text-sm font-medium text-text-primary truncate">
|
|
623
|
+
{member.displayName || member.name}
|
|
624
|
+
</span>
|
|
625
|
+
{member.isAgent && (
|
|
626
|
+
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-accent-cyan/20 text-accent-cyan rounded">
|
|
627
|
+
Agent
|
|
628
|
+
</span>
|
|
629
|
+
)}
|
|
630
|
+
{member.role === 'admin' && (
|
|
631
|
+
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-warning/20 text-warning rounded">
|
|
632
|
+
Admin
|
|
633
|
+
</span>
|
|
634
|
+
)}
|
|
635
|
+
</div>
|
|
636
|
+
<p className="text-xs text-text-muted">
|
|
637
|
+
Joined {new Date(member.joinedAt).toLocaleDateString()}
|
|
638
|
+
</p>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
{/* Actions */}
|
|
642
|
+
{isAdmin && !isSelf && (
|
|
643
|
+
<div className="relative">
|
|
644
|
+
<button
|
|
645
|
+
onClick={() => setShowMenu(!showMenu)}
|
|
646
|
+
className="w-8 h-8 rounded-lg flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-sidebar-border opacity-0 group-hover:opacity-100 transition-all"
|
|
647
|
+
>
|
|
648
|
+
<MoreIcon />
|
|
649
|
+
</button>
|
|
650
|
+
{showMenu && (
|
|
651
|
+
<>
|
|
652
|
+
<div
|
|
653
|
+
className="fixed inset-0 z-10"
|
|
654
|
+
onClick={() => setShowMenu(false)}
|
|
655
|
+
/>
|
|
656
|
+
<div className="absolute right-0 top-full mt-1 w-40 bg-sidebar-bg border border-sidebar-border rounded-lg shadow-lg z-20 py-1">
|
|
657
|
+
<button
|
|
658
|
+
onClick={() => {
|
|
659
|
+
onToggleRole();
|
|
660
|
+
setShowMenu(false);
|
|
661
|
+
}}
|
|
662
|
+
className="w-full px-3 py-2 text-sm text-left text-text-primary hover:bg-sidebar-border transition-colors"
|
|
663
|
+
>
|
|
664
|
+
{member.role === 'admin' ? 'Remove Admin' : 'Make Admin'}
|
|
665
|
+
</button>
|
|
666
|
+
<button
|
|
667
|
+
onClick={() => {
|
|
668
|
+
onRemove();
|
|
669
|
+
setShowMenu(false);
|
|
670
|
+
}}
|
|
671
|
+
className="w-full px-3 py-2 text-sm text-left text-error hover:bg-sidebar-border transition-colors"
|
|
672
|
+
>
|
|
673
|
+
Remove from Channel
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
</>
|
|
677
|
+
)}
|
|
678
|
+
</div>
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Icons
|
|
685
|
+
function SettingsIcon() {
|
|
686
|
+
return (
|
|
687
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
688
|
+
<circle cx="12" cy="12" r="3" />
|
|
689
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
690
|
+
</svg>
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function CloseIcon() {
|
|
695
|
+
return (
|
|
696
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
697
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
698
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
699
|
+
</svg>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function SearchIcon({ className = '' }: { className?: string }) {
|
|
704
|
+
return (
|
|
705
|
+
<svg className={className} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
706
|
+
<circle cx="11" cy="11" r="8" />
|
|
707
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
708
|
+
</svg>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function PlusIcon({ className = '' }: { className?: string }) {
|
|
713
|
+
return (
|
|
714
|
+
<svg className={className} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
715
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
716
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
717
|
+
</svg>
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function MoreIcon() {
|
|
722
|
+
return (
|
|
723
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
724
|
+
<circle cx="12" cy="12" r="1" fill="currentColor" />
|
|
725
|
+
<circle cx="19" cy="12" r="1" fill="currentColor" />
|
|
726
|
+
<circle cx="5" cy="12" r="1" fill="currentColor" />
|
|
727
|
+
</svg>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function LoadingSpinner() {
|
|
732
|
+
return (
|
|
733
|
+
<svg className="animate-spin mx-auto text-accent-cyan" width="24" height="24" viewBox="0 0 24 24">
|
|
734
|
+
<circle
|
|
735
|
+
cx="12"
|
|
736
|
+
cy="12"
|
|
737
|
+
r="10"
|
|
738
|
+
stroke="currentColor"
|
|
739
|
+
strokeWidth="2"
|
|
740
|
+
fill="none"
|
|
741
|
+
strokeDasharray="32"
|
|
742
|
+
strokeLinecap="round"
|
|
743
|
+
/>
|
|
744
|
+
</svg>
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function MiniSpinner() {
|
|
749
|
+
return (
|
|
750
|
+
<svg className="animate-spin" width="12" height="12" viewBox="0 0 24 24">
|
|
751
|
+
<circle
|
|
752
|
+
cx="12"
|
|
753
|
+
cy="12"
|
|
754
|
+
r="10"
|
|
755
|
+
stroke="currentColor"
|
|
756
|
+
strokeWidth="2"
|
|
757
|
+
fill="none"
|
|
758
|
+
strokeDasharray="32"
|
|
759
|
+
strokeLinecap="round"
|
|
760
|
+
/>
|
|
761
|
+
</svg>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function ErrorIcon({ className = '' }: { className?: string }) {
|
|
766
|
+
return (
|
|
767
|
+
<svg className={`mx-auto text-error ${className}`} width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
768
|
+
<circle cx="12" cy="12" r="10" />
|
|
769
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
770
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
771
|
+
</svg>
|
|
772
|
+
);
|
|
773
|
+
}
|