fix tab manager ux issues
This commit is contained in:
450
report.md
Normal file
450
report.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# Claudia UI/UX Research Report
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This research report analyzes the current state of Claudia's user interface and user experience, identifying key areas for improvement to enhance usability, accessibility, and overall user satisfaction. Based on comprehensive codebase analysis, user journey mapping, and modern UX best practices, we present actionable recommendations prioritized by impact and implementation complexity.
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### Architecture Strengths
|
||||||
|
|
||||||
|
Claudia demonstrates several strong architectural decisions:
|
||||||
|
|
||||||
|
- **Modern Tech Stack**: React 18 + TypeScript + Tauri 2 with excellent performance monitoring
|
||||||
|
- **Component Architecture**: Well-organized UI component library based on Radix UI primitives
|
||||||
|
- **State Management**: Zustand stores with TypeScript integration for predictable state updates
|
||||||
|
- **Animation System**: Framer Motion providing smooth, engaging transitions
|
||||||
|
- **Analytics Integration**: Comprehensive user behavior tracking with PostHog
|
||||||
|
|
||||||
|
### User Interface Assessment
|
||||||
|
|
||||||
|
#### **Navigation System**
|
||||||
|
The application employs a hybrid navigation approach:
|
||||||
|
- **Tab-based interface** for multi-tasking workflows
|
||||||
|
- **View-based routing** for different application sections
|
||||||
|
- **Drag-and-drop tab reordering** with keyboard shortcuts
|
||||||
|
|
||||||
|
**Current Navigation Flow:**
|
||||||
|
```
|
||||||
|
Welcome Screen → CC Agents/Projects Selection → Specific Workflows → Task Execution
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Visual Design System**
|
||||||
|
- Consistent theming with CSS custom properties
|
||||||
|
- Tailwind CSS v4 for rapid styling
|
||||||
|
- Dark/light mode support
|
||||||
|
- Cohesive iconography using Lucide React
|
||||||
|
|
||||||
|
## Critical Issues Identified
|
||||||
|
|
||||||
|
### 1. **Accessibility Crisis** 🚨 **HIGH PRIORITY**
|
||||||
|
|
||||||
|
**Issue**: Complete removal of focus indicators violates accessibility standards:
|
||||||
|
```css
|
||||||
|
/* Current - Accessibility violation */
|
||||||
|
* {
|
||||||
|
outline: none !important;
|
||||||
|
outline-offset: 0 !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
- Unusable for keyboard navigation users
|
||||||
|
- Violates WCAG 2.1 guidelines
|
||||||
|
- Legal compliance issues for enterprise adoption
|
||||||
|
|
||||||
|
**Recommendation**: Implement accessible focus management with proper visual indicators.
|
||||||
|
|
||||||
|
### 2. **Mobile Experience Gap** 📱 **HIGH PRIORITY**
|
||||||
|
|
||||||
|
**Current State**: Desktop-first design with limited responsive adaptations
|
||||||
|
- Fixed sidebars don't adapt to mobile viewports
|
||||||
|
- Touch interactions not optimized
|
||||||
|
- Navigation patterns unsuitable for mobile users
|
||||||
|
|
||||||
|
**Usage Patterns**:
|
||||||
|
- Tab management requires precise cursor control
|
||||||
|
- Small tap targets (< 44px) throughout interface
|
||||||
|
- No gesture support for common mobile interactions
|
||||||
|
|
||||||
|
### 3. **User Onboarding Absence** 🎯 **HIGH PRIORITY**
|
||||||
|
|
||||||
|
**Current Experience**: New users face a blank slate without guidance
|
||||||
|
- No welcome tutorial or feature introduction
|
||||||
|
- Complex features lack contextual help
|
||||||
|
- Keyboard shortcuts not discoverable
|
||||||
|
|
||||||
|
### 4. **Error Communication Deficiency** ⚠️ **MEDIUM PRIORITY**
|
||||||
|
|
||||||
|
**Current Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Generic error handling throughout codebase
|
||||||
|
catch (err) {
|
||||||
|
console.error("Failed to load agents:", err);
|
||||||
|
setError("Failed to load agents");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues**:
|
||||||
|
- Technical error messages shown to users
|
||||||
|
- No recovery suggestions provided
|
||||||
|
- Limited context about error causes
|
||||||
|
|
||||||
|
## Detailed Improvement Recommendations
|
||||||
|
|
||||||
|
### Phase 1: Foundation Fixes (2-3 weeks)
|
||||||
|
|
||||||
|
#### **1.1 Accessibility Restoration**
|
||||||
|
```typescript
|
||||||
|
// Implement accessible focus management
|
||||||
|
const FocusManager = {
|
||||||
|
enableFocusVisible: () => {
|
||||||
|
document.body.classList.add('focus-visible-enabled');
|
||||||
|
},
|
||||||
|
|
||||||
|
trapFocus: (container: HTMLElement) => {
|
||||||
|
// Implement focus trap for modals and dialogs
|
||||||
|
},
|
||||||
|
|
||||||
|
announceToScreen Reader: (message: string) => {
|
||||||
|
// Live region announcements for dynamic content
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- Remove global outline removal
|
||||||
|
- Add focus-visible polyfill for modern focus management
|
||||||
|
- Implement focus traps for modal dialogs
|
||||||
|
- Add ARIA labels and roles throughout
|
||||||
|
|
||||||
|
#### **1.2 Mobile Responsive Foundation**
|
||||||
|
```scss
|
||||||
|
// Implement mobile-first responsive design
|
||||||
|
.tab-manager {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
&.mobile-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mobile Navigation Pattern**:
|
||||||
|
- Collapsible sidebar navigation
|
||||||
|
- Bottom sheet for tab management
|
||||||
|
- Swipe gestures for tab switching
|
||||||
|
- Touch-optimized button sizes (minimum 44px)
|
||||||
|
|
||||||
|
#### **1.3 Error Handling Enhancement**
|
||||||
|
```typescript
|
||||||
|
// User-friendly error system
|
||||||
|
interface UserError {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
actions: ErrorAction[];
|
||||||
|
context?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUserError = (error: Error, context: string): UserError => {
|
||||||
|
const errorMap = {
|
||||||
|
'CLAUDE_NOT_FOUND': {
|
||||||
|
title: 'Claude Not Found',
|
||||||
|
message: 'Claude Code CLI is not installed or not in your PATH.',
|
||||||
|
actions: [
|
||||||
|
{ label: 'Download Claude', action: () => openClaudeDownload() },
|
||||||
|
{ label: 'Set Custom Path', action: () => openPathDialog() }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return errorMap[error.code] || getGenericError(error);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: User Experience Enhancements (3-4 weeks)
|
||||||
|
|
||||||
|
#### **2.1 Interactive Onboarding System**
|
||||||
|
```typescript
|
||||||
|
// Progressive onboarding with contextual tips
|
||||||
|
const OnboardingProvider = ({ children }) => {
|
||||||
|
const [tourStep, setTourStep] = useState(0);
|
||||||
|
const [hasCompletedTour, setHasCompletedTour] = useState(false);
|
||||||
|
|
||||||
|
const tourSteps = [
|
||||||
|
{
|
||||||
|
target: '[data-tour="agents"]',
|
||||||
|
title: 'Create Your First Agent',
|
||||||
|
content: 'CC Agents are specialized AI assistants...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="projects"]',
|
||||||
|
title: 'Manage Your Projects',
|
||||||
|
content: 'Browse and manage your Claude Code sessions...'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TourContext.Provider value={{ tourStep, setTourStep }}>
|
||||||
|
{children}
|
||||||
|
{!hasCompletedTour && <InteractiveTour steps={tourSteps} />}
|
||||||
|
</TourContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2.2 Progressive Disclosure Interface**
|
||||||
|
```typescript
|
||||||
|
// Reduce cognitive load with progressive disclosure
|
||||||
|
const AgentCard = ({ agent }) => {
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
{/* Essential information always visible */}
|
||||||
|
<h3>{agent.name}</h3>
|
||||||
|
<p>{agent.description}</p>
|
||||||
|
|
||||||
|
{/* Advanced options revealed on demand */}
|
||||||
|
<Collapsible open={showAdvanced}>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<HooksConfiguration agent={agent} />
|
||||||
|
<ModelSettings agent={agent} />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
>
|
||||||
|
{showAdvanced ? 'Less Options' : 'More Options'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2.3 Contextual Help System**
|
||||||
|
```typescript
|
||||||
|
// Context-sensitive help integration
|
||||||
|
const HelpProvider = () => {
|
||||||
|
const [helpContext, setHelpContext] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const contextualHelp = {
|
||||||
|
'agent-creation': {
|
||||||
|
title: 'Creating CC Agents',
|
||||||
|
content: 'Learn how to create specialized AI agents...',
|
||||||
|
videoUrl: '/help/agent-creation.mp4',
|
||||||
|
links: [
|
||||||
|
{ text: 'Agent Templates', url: '/help/templates' },
|
||||||
|
{ text: 'Best Practices', url: '/help/best-practices' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HelpContext.Provider value={{ contextualHelp, setHelpContext }}>
|
||||||
|
{helpContext && (
|
||||||
|
<HelpDrawer context={contextualHelp[helpContext]} />
|
||||||
|
)}
|
||||||
|
</HelpContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Advanced User Experience (4-5 weeks)
|
||||||
|
|
||||||
|
#### **3.1 Intelligent State Management**
|
||||||
|
```typescript
|
||||||
|
// Persistent user preferences and session recovery
|
||||||
|
const UserPreferencesStore = create<UserPreferences>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
theme: 'system',
|
||||||
|
sidebarCollapsed: false,
|
||||||
|
recentProjects: [],
|
||||||
|
keyboardShortcuts: defaultShortcuts,
|
||||||
|
|
||||||
|
// Smart defaults based on usage patterns
|
||||||
|
suggestProjects: () => {
|
||||||
|
const { recentProjects, usage } = get();
|
||||||
|
return analyzeUsagePatterns(recentProjects, usage);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'claudia-preferences',
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3.2 Performance Optimization**
|
||||||
|
```typescript
|
||||||
|
// Virtual scrolling for large datasets
|
||||||
|
const VirtualizedAgentList = ({ agents }) => {
|
||||||
|
const parentRef = useRef();
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: agents.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 120,
|
||||||
|
overscan: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parentRef} className="virtual-list-container">
|
||||||
|
<div style={{ height: virtualizer.getTotalSize() }}>
|
||||||
|
{virtualizer.getVirtualItems().map(virtualItem => (
|
||||||
|
<AgentCard
|
||||||
|
key={virtualItem.key}
|
||||||
|
agent={agents[virtualItem.index]}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
transform: `translateY(${virtualItem.start}px)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3.3 Advanced Interaction Patterns**
|
||||||
|
```typescript
|
||||||
|
// Command palette for power users
|
||||||
|
const CommandPalette = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const commands = useMemo(() => [
|
||||||
|
{
|
||||||
|
id: 'create-agent',
|
||||||
|
title: 'Create New Agent',
|
||||||
|
description: 'Create a new CC Agent',
|
||||||
|
shortcut: 'Cmd+Shift+A',
|
||||||
|
action: () => navigateToAgentCreation()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-project',
|
||||||
|
title: 'Open Project',
|
||||||
|
description: 'Open a Claude Code project',
|
||||||
|
shortcut: 'Cmd+O',
|
||||||
|
action: () => openProjectDialog()
|
||||||
|
}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
const filteredCommands = useMemo(() =>
|
||||||
|
commands.filter(cmd =>
|
||||||
|
cmd.title.toLowerCase().includes(query.toLowerCase())
|
||||||
|
), [commands, query]
|
||||||
|
);
|
||||||
|
|
||||||
|
useKeyboard('cmd+k', () => setOpen(true));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Type a command or search..."
|
||||||
|
value={query}
|
||||||
|
onValueChange={setQuery}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
{filteredCommands.map(command => (
|
||||||
|
<CommandItem key={command.id} onSelect={command.action}>
|
||||||
|
<span>{command.title}</span>
|
||||||
|
<CommandShortcut>{command.shortcut}</CommandShortcut>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
### **Month 1: Critical Fixes**
|
||||||
|
- Week 1-2: Accessibility restoration and focus management
|
||||||
|
- Week 3-4: Mobile responsive design implementation
|
||||||
|
|
||||||
|
### **Month 2: User Experience**
|
||||||
|
- Week 5-6: Onboarding system and contextual help
|
||||||
|
- Week 7-8: Error handling enhancement and user feedback
|
||||||
|
|
||||||
|
### **Month 3: Advanced Features**
|
||||||
|
- Week 9-10: Performance optimization and virtual scrolling
|
||||||
|
- Week 11-12: Command palette and power user features
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### **Accessibility Metrics**
|
||||||
|
- **Keyboard Navigation**: 100% of features accessible via keyboard
|
||||||
|
- **Screen Reader Compatibility**: All critical paths announced properly
|
||||||
|
- **Focus Management**: Clear focus indicators on all interactive elements
|
||||||
|
|
||||||
|
### **User Experience Metrics**
|
||||||
|
- **Time to First Success**: < 5 minutes for new users to complete first agent creation
|
||||||
|
- **Mobile Usage**: > 20% increase in mobile/tablet sessions
|
||||||
|
- **Error Recovery Rate**: > 80% of users successfully recover from errors
|
||||||
|
- **Feature Discovery**: > 60% of users discover keyboard shortcuts within first week
|
||||||
|
|
||||||
|
### **Performance Metrics**
|
||||||
|
- **Initial Load Time**: < 3 seconds for application startup
|
||||||
|
- **Interaction Response**: < 100ms for all UI interactions
|
||||||
|
- **Memory Usage**: < 500MB for typical usage sessions
|
||||||
|
|
||||||
|
## Technical Considerations
|
||||||
|
|
||||||
|
### **Accessibility Standards Compliance**
|
||||||
|
- WCAG 2.1 AA compliance for all interactive elements
|
||||||
|
- Screen reader testing with NVDA, JAWS, and VoiceOver
|
||||||
|
- Color contrast ratios meeting minimum 4.5:1 standard
|
||||||
|
|
||||||
|
### **Performance Requirements**
|
||||||
|
- Virtual scrolling for lists > 100 items
|
||||||
|
- React.memo optimization for frequently re-rendering components
|
||||||
|
- Lazy loading for non-critical UI components
|
||||||
|
|
||||||
|
### **Mobile Optimization**
|
||||||
|
- Touch targets minimum 44px × 44px
|
||||||
|
- Gesture support for common interactions
|
||||||
|
- Progressive web app capabilities for mobile installation
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### **High Risk**
|
||||||
|
- **Accessibility Changes**: Potential to break existing user workflows
|
||||||
|
- **Mobile Redesign**: Significant architectural changes required
|
||||||
|
|
||||||
|
### **Medium Risk**
|
||||||
|
- **Performance Optimization**: May introduce new bugs in virtual scrolling
|
||||||
|
- **State Management Changes**: Could affect data persistence
|
||||||
|
|
||||||
|
### **Low Risk**
|
||||||
|
- **Error Message Improvements**: Purely additive changes
|
||||||
|
- **Onboarding System**: Optional feature that doesn't affect core functionality
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Claudia has a solid technical foundation but requires significant user experience improvements to reach its full potential. The recommended phased approach prioritizes critical accessibility fixes while building toward a more intuitive, mobile-friendly, and user-centric experience.
|
||||||
|
|
||||||
|
The most impactful improvements focus on:
|
||||||
|
1. **Accessibility restoration** for inclusive design
|
||||||
|
2. **Mobile optimization** for broader device support
|
||||||
|
3. **User onboarding** for improved adoption
|
||||||
|
4. **Error communication** for better user confidence
|
||||||
|
|
||||||
|
Implementing these recommendations will transform Claudia from a technically capable tool into a truly user-friendly application that empowers developers to work more effectively with Claude Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Report generated on: $(date)*
|
||||||
|
*Codebase analysis version: v0.1.0*
|
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import { motion, AnimatePresence, Reorder } from 'framer-motion';
|
import { motion, AnimatePresence, Reorder } from 'framer-motion';
|
||||||
import { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings, FileText } from 'lucide-react';
|
import { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings, FileText } from 'lucide-react';
|
||||||
import { useTabState } from '@/hooks/useTabState';
|
import { useTabState } from '@/hooks/useTabState';
|
||||||
import { Tab } from '@/contexts/TabContext';
|
import { Tab, useTabContext } from '@/contexts/TabContext';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTrackEvent } from '@/hooks';
|
import { useTrackEvent } from '@/hooks';
|
||||||
|
|
||||||
@@ -11,9 +11,11 @@ interface TabItemProps {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onClose: (id: string) => void;
|
onClose: (id: string) => void;
|
||||||
onClick: (id: string) => void;
|
onClick: (id: string) => void;
|
||||||
|
isDragging?: boolean;
|
||||||
|
setDraggedTabId?: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick }) => {
|
const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick, isDragging = false, setDraggedTabId }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
@@ -62,56 +64,68 @@ const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick }) =>
|
|||||||
<Reorder.Item
|
<Reorder.Item
|
||||||
value={tab}
|
value={tab}
|
||||||
id={tab.id}
|
id={tab.id}
|
||||||
|
dragListener={true}
|
||||||
|
transition={{ duration: 0.1 }} // Snappy reorder animation
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer select-none",
|
"relative flex items-center gap-2 text-sm cursor-pointer select-none group",
|
||||||
"border-b-2 transition-all duration-200",
|
"transition-colors duration-100 overflow-hidden border-r border-border/20",
|
||||||
|
"before:absolute before:bottom-0 before:left-0 before:right-0 before:h-0.5 before:transition-colors before:duration-100",
|
||||||
isActive
|
isActive
|
||||||
? "border-blue-500 bg-background text-foreground"
|
? "bg-background text-foreground before:bg-accent"
|
||||||
: "border-transparent bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground",
|
: "bg-muted/30 text-muted-foreground hover:bg-muted/50 hover:text-foreground before:bg-transparent",
|
||||||
"min-w-[120px] max-w-[200px]"
|
isDragging && "bg-background border-accent/50 shadow-sm z-50",
|
||||||
|
"min-w-[120px] max-w-[220px] h-8 px-3"
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
onClick={() => onClick(tab.id)}
|
onClick={() => onClick(tab.id)}
|
||||||
whileHover={{ y: -1 }}
|
onDragStart={() => setDraggedTabId?.(tab.id)}
|
||||||
whileTap={{ scale: 0.98 }}
|
onDragEnd={() => setDraggedTabId?.(null)}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
{/* Tab Icon */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className="flex-1 truncate">
|
{/* Tab Title */}
|
||||||
|
<span className="flex-1 truncate text-xs font-medium min-w-0">
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Status Indicators - always takes up space */}
|
||||||
|
<div className="flex items-center gap-1.5 flex-shrink-0 w-6 justify-end">
|
||||||
{statusIcon && (
|
{statusIcon && (
|
||||||
<span className="flex-shrink-0">
|
<span className="flex items-center justify-center">
|
||||||
{statusIcon}
|
{statusIcon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab.hasUnsavedChanges && (
|
{tab.hasUnsavedChanges && !statusIcon && (
|
||||||
<span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
|
<span
|
||||||
|
className="w-1.5 h-1.5 bg-accent rounded-full"
|
||||||
|
title="Unsaved changes"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
{/* Close Button - Always reserves space */}
|
||||||
{(isHovered || isActive) && (
|
<button
|
||||||
<motion.button
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClose(tab.id);
|
onClose(tab.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 p-0.5 rounded hover:bg-muted-foreground/20",
|
"flex-shrink-0 w-4 h-4 flex items-center justify-center rounded-sm",
|
||||||
"transition-colors duration-150"
|
"transition-all duration-100 hover:bg-destructive/20 hover:text-destructive",
|
||||||
|
"focus:outline-none focus:ring-1 focus:ring-destructive/50",
|
||||||
|
(isHovered || isActive) ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
|
title={`Close ${tab.title}`}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</motion.button>
|
</button>
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Reorder.Item>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -131,9 +145,13 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
canAddTab
|
canAddTab
|
||||||
} = useTabState();
|
} = useTabState();
|
||||||
|
|
||||||
|
// Access reorderTabs from context
|
||||||
|
const { reorderTabs } = useTabContext();
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
||||||
const [showRightScroll, setShowRightScroll] = useState(false);
|
const [showRightScroll, setShowRightScroll] = useState(false);
|
||||||
|
const [draggedTabId, setDraggedTabId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Analytics tracking
|
// Analytics tracking
|
||||||
const trackEvent = useTrackEvent();
|
const trackEvent = useTrackEvent();
|
||||||
@@ -231,8 +249,26 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
}, [tabs]);
|
}, [tabs]);
|
||||||
|
|
||||||
const handleReorder = (newOrder: Tab[]) => {
|
const handleReorder = (newOrder: Tab[]) => {
|
||||||
// This will be handled by the context when we implement reorderTabs
|
// Find the positions that changed
|
||||||
console.log('Reorder tabs:', newOrder);
|
const oldOrder = tabs.map(tab => tab.id);
|
||||||
|
const newOrderIds = newOrder.map(tab => tab.id);
|
||||||
|
|
||||||
|
// Find what moved
|
||||||
|
const movedTabId = newOrderIds.find((id, index) => oldOrder[index] !== id);
|
||||||
|
if (!movedTabId) return;
|
||||||
|
|
||||||
|
const oldIndex = oldOrder.indexOf(movedTabId);
|
||||||
|
const newIndex = newOrderIds.indexOf(movedTabId);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||||
|
// Use the context's reorderTabs function
|
||||||
|
reorderTabs(oldIndex, newIndex);
|
||||||
|
// Track the reorder event
|
||||||
|
trackEvent.featureUsed?.('tab_reorder', 'drag_drop', {
|
||||||
|
from_index: oldIndex,
|
||||||
|
to_index: newIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = async (id: string) => {
|
const handleCloseTab = async (id: string) => {
|
||||||
@@ -266,7 +302,12 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center bg-muted/30 border-b", className)}>
|
<div className={cn("flex items-stretch bg-muted/20 border-b relative", className)}>
|
||||||
|
{/* Left fade gradient */}
|
||||||
|
{showLeftScroll && (
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-muted/20 to-transparent pointer-events-none z-10" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Left scroll button */}
|
{/* Left scroll button */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showLeftScroll && (
|
{showLeftScroll && (
|
||||||
@@ -275,9 +316,14 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
onClick={() => scrollTabs('left')}
|
onClick={() => scrollTabs('left')}
|
||||||
className="p-1 hover:bg-muted rounded-sm"
|
className={cn(
|
||||||
|
"p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1",
|
||||||
|
"transition-colors duration-200 flex items-center justify-center",
|
||||||
|
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
|
||||||
|
)}
|
||||||
|
title="Scroll tabs left"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
<path d="M15 18l-6-6 6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M15 18l-6-6 6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -294,9 +340,9 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
axis="x"
|
axis="x"
|
||||||
values={tabs}
|
values={tabs}
|
||||||
onReorder={handleReorder}
|
onReorder={handleReorder}
|
||||||
className="flex items-stretch"
|
className="flex items-stretch h-8"
|
||||||
|
layoutScroll={false}
|
||||||
>
|
>
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabItem
|
<TabItem
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -304,12 +350,18 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
isActive={tab.id === activeTabId}
|
isActive={tab.id === activeTabId}
|
||||||
onClose={handleCloseTab}
|
onClose={handleCloseTab}
|
||||||
onClick={switchToTab}
|
onClick={switchToTab}
|
||||||
|
isDragging={draggedTabId === tab.id}
|
||||||
|
setDraggedTabId={setDraggedTabId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
|
||||||
</Reorder.Group>
|
</Reorder.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right fade gradient */}
|
||||||
|
{showRightScroll && (
|
||||||
|
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-muted/20 to-transparent pointer-events-none z-10" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Right scroll button */}
|
{/* Right scroll button */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showRightScroll && (
|
{showRightScroll && (
|
||||||
@@ -318,9 +370,14 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
onClick={() => scrollTabs('right')}
|
onClick={() => scrollTabs('right')}
|
||||||
className="p-1 hover:bg-muted rounded-sm"
|
className={cn(
|
||||||
|
"p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1",
|
||||||
|
"transition-colors duration-200 flex items-center justify-center",
|
||||||
|
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
|
||||||
|
)}
|
||||||
|
title="Scroll tabs right"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
<path d="M9 18l6-6-6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M9 18l6-6-6-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -328,21 +385,20 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* New tab button */}
|
{/* New tab button */}
|
||||||
<motion.button
|
<button
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
onClick={handleNewTab}
|
onClick={handleNewTab}
|
||||||
disabled={!canAddTab()}
|
disabled={!canAddTab()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1.5 mx-2 rounded-sm transition-colors",
|
"p-2 mx-2 rounded-md transition-all duration-200 flex items-center justify-center",
|
||||||
|
"border border-border/50 bg-background/50 backdrop-blur-sm",
|
||||||
canAddTab()
|
canAddTab()
|
||||||
? "hover:bg-muted text-muted-foreground hover:text-foreground"
|
? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm"
|
||||||
: "opacity-50 cursor-not-allowed"
|
: "opacity-50 cursor-not-allowed bg-muted/30"
|
||||||
)}
|
)}
|
||||||
title={canAddTab() ? "Browse projects" : "Maximum tabs reached"}
|
title={canAddTab() ? "Browse projects (Ctrl+T)" : `Maximum tabs reached (${tabs.length}/20)`}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
</motion.button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,15 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Custom scrollbar hiding */
|
||||||
|
.scrollbar-hide {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark theme configuration */
|
/* Dark theme configuration */
|
||||||
@theme {
|
@theme {
|
||||||
/* Colors */
|
/* Colors */
|
||||||
|
Reference in New Issue
Block a user