Add interactive dropdown selectors to SpaceBreadcrumb component

This commit is contained in:
spencrmartin
2025-12-03 15:33:01 -05:00
parent 857144df61
commit fffd9dd975
2 changed files with 159 additions and 16 deletions
+137 -15
View File
@@ -1,11 +1,13 @@
import React, { useState, useEffect } from 'react';
import { ChevronRight, Hash } from 'lucide-react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { ChevronRight, Hash, ChevronDown, Layers } from 'lucide-react';
import { matrixService } from '../services/MatrixService';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
interface SpaceBreadcrumbProps {
roomId: string;
className?: string;
onSpaceClick?: (spaceId: string) => void;
onRoomClick?: (roomId: string) => void;
}
interface BreadcrumbData {
@@ -15,14 +17,27 @@ interface BreadcrumbData {
roomId: string;
}
export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({ roomId, className = '' }) => {
export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({
roomId,
className = '',
onSpaceClick,
onRoomClick
}) => {
const [breadcrumb, setBreadcrumb] = useState<BreadcrumbData | null>(null);
const [showSpaceDropdown, setShowSpaceDropdown] = useState(false);
const [showRoomDropdown, setShowRoomDropdown] = useState(false);
const [allSpaces, setAllSpaces] = useState<Array<{ roomId: string; name: string }>>([]);
const [spaceRooms, setSpaceRooms] = useState<Array<{ roomId: string; name: string }>>([]);
const spaceDropdownRef = useRef<HTMLDivElement>(null);
const roomDropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const loadBreadcrumb = () => {
try {
// Get all Spaces
const spaces = matrixService.getSpaces();
setAllSpaces(spaces.map(s => ({ roomId: s.roomId, name: s.name || 'Unnamed Space' })));
// Find which Space contains this room
for (const space of spaces) {
@@ -37,6 +52,12 @@ export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({ roomId, classN
roomName: childRoom.name || 'Unnamed Room',
roomId: roomId,
});
// Load all rooms in this Space
setSpaceRooms(children.map(c => ({
roomId: c.roomId,
name: c.name || 'Unnamed Room'
})));
return;
}
}
@@ -67,6 +88,31 @@ export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({ roomId, classN
};
}, [roomId]);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (spaceDropdownRef.current && !spaceDropdownRef.current.contains(event.target as Node)) {
setShowSpaceDropdown(false);
}
if (roomDropdownRef.current && !roomDropdownRef.current.contains(event.target as Node)) {
setShowRoomDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSpaceSelect = useCallback((spaceId: string) => {
setShowSpaceDropdown(false);
onSpaceClick?.(spaceId);
}, [onSpaceClick]);
const handleRoomSelect = useCallback((selectedRoomId: string) => {
setShowRoomDropdown(false);
onRoomClick?.(selectedRoomId);
}, [onRoomClick]);
// Don't render if room is not in a Space
if (!breadcrumb) {
return null;
@@ -79,24 +125,100 @@ export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({ roomId, classN
transition={{ duration: 0.2 }}
className={`flex items-center gap-2 px-4 py-2 bg-background-muted border-b border-border-default ${className}`}
>
{/* Space Icon */}
<div className="flex items-center gap-2 text-text-muted">
<div className="w-5 h-5 bg-background-accent/20 rounded flex items-center justify-center">
<Hash className="w-3 h-3 text-text-on-accent" />
</div>
<span className="text-sm font-medium">{breadcrumb.spaceName}</span>
{/* Space Selector */}
<div className="relative" ref={spaceDropdownRef}>
<button
onClick={() => setShowSpaceDropdown(!showSpaceDropdown)}
className="flex items-center gap-2 text-text-muted hover:text-text-default transition-colors rounded px-2 py-1 hover:bg-background-hover"
aria-label="Select space"
aria-expanded={showSpaceDropdown}
>
<div className="w-5 h-5 bg-background-accent/20 rounded flex items-center justify-center">
<Layers className="w-3 h-3 text-text-on-accent" />
</div>
<span className="text-sm font-medium">{breadcrumb.spaceName}</span>
<ChevronDown className="w-3 h-3" />
</button>
<AnimatePresence>
{showSpaceDropdown && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.15 }}
className="absolute top-full left-0 mt-1 w-64 bg-background-elevated border border-border-default rounded-lg shadow-lg overflow-hidden z-50"
>
<div className="max-h-80 overflow-y-auto">
{allSpaces.map((space) => (
<button
key={space.roomId}
onClick={() => handleSpaceSelect(space.roomId)}
className={`w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-background-hover transition-colors ${
space.roomId === breadcrumb.spaceId ? 'bg-background-accent/10 text-text-on-accent' : 'text-text-default'
}`}
>
<Layers className="w-4 h-4 flex-shrink-0" />
<span className="text-sm truncate">{space.name}</span>
{space.roomId === breadcrumb.spaceId && (
<span className="ml-auto text-xs text-text-muted">Current</span>
)}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Separator */}
<ChevronRight className="w-4 h-4 text-text-muted" />
{/* Room Name */}
<div className="flex items-center gap-2">
<Hash className="w-4 h-4 text-text-muted" />
<span className="text-sm text-text-default">{breadcrumb.roomName}</span>
{/* Room Selector */}
<div className="relative" ref={roomDropdownRef}>
<button
onClick={() => setShowRoomDropdown(!showRoomDropdown)}
className="flex items-center gap-2 text-text-default hover:text-text-emphasis transition-colors rounded px-2 py-1 hover:bg-background-hover"
aria-label="Select room"
aria-expanded={showRoomDropdown}
>
<Hash className="w-4 h-4" />
<span className="text-sm">{breadcrumb.roomName}</span>
<ChevronDown className="w-3 h-3" />
</button>
<AnimatePresence>
{showRoomDropdown && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.15 }}
className="absolute top-full left-0 mt-1 w-64 bg-background-elevated border border-border-default rounded-lg shadow-lg overflow-hidden z-50"
>
<div className="max-h-80 overflow-y-auto">
{spaceRooms.map((room) => (
<button
key={room.roomId}
onClick={() => handleRoomSelect(room.roomId)}
className={`w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-background-hover transition-colors ${
room.roomId === breadcrumb.roomId ? 'bg-background-accent/10 text-text-on-accent' : 'text-text-default'
}`}
>
<Hash className="w-4 h-4 flex-shrink-0" />
<span className="text-sm truncate">{room.name}</span>
{room.roomId === breadcrumb.roomId && (
<span className="ml-auto text-xs text-text-muted">Current</span>
)}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Optional: Add a badge to indicate it's a Space room */}
{/* Space Room Badge */}
<div className="ml-auto">
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400">
Space Room
@@ -7,6 +7,7 @@ import { useTabContext } from '../contexts/TabContext';
import { ResizableSplitter } from './Layout/ResizableSplitter';
import { TaskExecutionProvider } from '../contexts/TaskExecutionContext';
import { SpaceBreadcrumb } from './SpaceBreadcrumb';
import { useNavigate } from 'react-router-dom';
interface TabbedChatContainerProps {
setIsGoosehintsModalOpen?: (isOpen: boolean) => void;
@@ -23,6 +24,7 @@ export const TabbedChatContainer: React.FC<TabbedChatContainerProps> = ({
initialMessage,
sidebarCollapsed = false
}) => {
const navigate = useNavigate();
const {
tabStates,
activeTabId,
@@ -35,11 +37,26 @@ export const TabbedChatContainer: React.FC<TabbedChatContainerProps> = ({
syncTabTitleWithBackend,
updateTabTitleFromMessage,
updateSessionId,
openMatrixChat,
// Sidecar functions
hideSidecarView,
getSidecarState
} = useTabContext();
// Handle room switching from breadcrumb
const handleRoomSwitch = useCallback((roomId: string) => {
console.log('🔄 Switching to room:', roomId);
openMatrixChat(roomId);
}, [openMatrixChat]);
// Handle space navigation from breadcrumb
const handleSpaceNavigation = useCallback((spaceId: string) => {
console.log('🏠 Navigating to space:', spaceId);
// TODO: Navigate to space view (e.g., SpaceRoomsView or ChannelsView)
// For now, we could navigate to the first room in the space
// navigate(`/channels/${spaceId}`);
}, [navigate]);
const handleMessageSubmitWrapper = useCallback(async (message: string, tabId: string) => {
// Find the tab state to check if this is the first message
const tabState = tabStates.find(ts => ts.tab.id === tabId);
@@ -204,7 +221,11 @@ export const TabbedChatContainer: React.FC<TabbedChatContainerProps> = ({
{/* Space Breadcrumb - Show for Matrix tabs with room IDs */}
{activeTabState && activeTabState.tab.type === 'matrix' && activeTabState.tab.matrixRoomId && (
<div className="flex-shrink-0">
<SpaceBreadcrumb roomId={activeTabState.tab.matrixRoomId} />
<SpaceBreadcrumb
roomId={activeTabState.tab.matrixRoomId}
onRoomClick={handleRoomSwitch}
onSpaceClick={handleSpaceNavigation}
/>
</div>
)}