fix tab manager ux issues

This commit is contained in:
Kiran Johns
2025-08-01 22:01:46 +05:30
committed by Vivek R
parent 74c2bb3746
commit 92599b373d
3 changed files with 584 additions and 68 deletions

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence, Reorder } from 'framer-motion';
import { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings, FileText } from 'lucide-react';
import { useTabState } from '@/hooks/useTabState';
import { Tab } from '@/contexts/TabContext';
import { Tab, useTabContext } from '@/contexts/TabContext';
import { cn } from '@/lib/utils';
import { useTrackEvent } from '@/hooks';
@@ -11,9 +11,11 @@ interface TabItemProps {
isActive: boolean;
onClose: (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 getIcon = () => {
@@ -62,56 +64,68 @@ const TabItem: React.FC<TabItemProps> = ({ tab, isActive, onClose, onClick }) =>
<Reorder.Item
value={tab}
id={tab.id}
dragListener={true}
transition={{ duration: 0.1 }} // Snappy reorder animation
className={cn(
"relative flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer select-none",
"border-b-2 transition-all duration-200",
"relative flex items-center gap-2 text-sm cursor-pointer select-none group",
"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
? "border-blue-500 bg-background text-foreground"
: "border-transparent bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground",
"min-w-[120px] max-w-[200px]"
? "bg-background text-foreground before:bg-accent"
: "bg-muted/30 text-muted-foreground hover:bg-muted/50 hover:text-foreground before:bg-transparent",
isDragging && "bg-background border-accent/50 shadow-sm z-50",
"min-w-[120px] max-w-[220px] h-8 px-3"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => onClick(tab.id)}
whileHover={{ y: -1 }}
whileTap={{ scale: 0.98 }}
onDragStart={() => setDraggedTabId?.(tab.id)}
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}
</span>
{statusIcon && (
<span className="flex-shrink-0">
{statusIcon}
</span>
)}
{tab.hasUnsavedChanges && (
<span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
)}
<AnimatePresence>
{(isHovered || isActive) && (
<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) => {
e.stopPropagation();
onClose(tab.id);
}}
className={cn(
"flex-shrink-0 p-0.5 rounded hover:bg-muted-foreground/20",
"transition-colors duration-150"
)}
>
<X className="w-3 h-3" />
</motion.button>
{/* Status Indicators - always takes up space */}
<div className="flex items-center gap-1.5 flex-shrink-0 w-6 justify-end">
{statusIcon && (
<span className="flex items-center justify-center">
{statusIcon}
</span>
)}
</AnimatePresence>
{tab.hasUnsavedChanges && !statusIcon && (
<span
className="w-1.5 h-1.5 bg-accent rounded-full"
title="Unsaved changes"
/>
)}
</div>
{/* Close Button - Always reserves space */}
<button
onClick={(e) => {
e.stopPropagation();
onClose(tab.id);
}}
className={cn(
"flex-shrink-0 w-4 h-4 flex items-center justify-center rounded-sm",
"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" />
</button>
</Reorder.Item>
);
};
@@ -131,9 +145,13 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
canAddTab
} = useTabState();
// Access reorderTabs from context
const { reorderTabs } = useTabContext();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftScroll, setShowLeftScroll] = useState(false);
const [showRightScroll, setShowRightScroll] = useState(false);
const [draggedTabId, setDraggedTabId] = useState<string | null>(null);
// Analytics tracking
const trackEvent = useTrackEvent();
@@ -231,8 +249,26 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
}, [tabs]);
const handleReorder = (newOrder: Tab[]) => {
// This will be handled by the context when we implement reorderTabs
console.log('Reorder tabs:', newOrder);
// Find the positions that changed
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) => {
@@ -266,7 +302,12 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
};
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 */}
<AnimatePresence>
{showLeftScroll && (
@@ -275,9 +316,14 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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" />
</svg>
</motion.button>
@@ -294,22 +340,28 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
axis="x"
values={tabs}
onReorder={handleReorder}
className="flex items-stretch"
className="flex items-stretch h-8"
layoutScroll={false}
>
<AnimatePresence initial={false}>
{tabs.map((tab) => (
<TabItem
key={tab.id}
tab={tab}
isActive={tab.id === activeTabId}
onClose={handleCloseTab}
onClick={switchToTab}
/>
))}
</AnimatePresence>
{tabs.map((tab) => (
<TabItem
key={tab.id}
tab={tab}
isActive={tab.id === activeTabId}
onClose={handleCloseTab}
onClick={switchToTab}
isDragging={draggedTabId === tab.id}
setDraggedTabId={setDraggedTabId}
/>
))}
</Reorder.Group>
</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 */}
<AnimatePresence>
{showRightScroll && (
@@ -318,9 +370,14 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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" />
</svg>
</motion.button>
@@ -328,21 +385,20 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
</AnimatePresence>
{/* New tab button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
<button
onClick={handleNewTab}
disabled={!canAddTab()}
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()
? "hover:bg-muted text-muted-foreground hover:text-foreground"
: "opacity-50 cursor-not-allowed"
? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm"
: "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" />
</motion.button>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
);
};

View File

@@ -1,5 +1,15 @@
@import "tailwindcss";
/* Custom scrollbar hiding */
.scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Dark theme configuration */
@theme {
/* Colors */