Files
iistwin/client/src/components/task-detail/TaskTabsBar.tsx
Ильяс Султанов 52b583c671 refactor(task-detail): разбиение TaskDetail.tsx (3278→804) — механическая экстракция
Шаг 2.2 (часть 2) плана production-готовности:
- 14 файлов в components/task-detail/ (типы, вкладки, диалоги, шапка, таб-бар,
  мобильный accordion, хук условий полей)
- посимвольная сверка: все несовпадения объяснены (export-префиксы/импорты/пропсы)
- check/lint/build чисто, vitest 104/104
2026-09-08 06:53:25 +03:00

170 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ChevronLeft, ChevronRight, Maximize2, Minimize2, FileText, MessageSquare } from 'lucide-react';
import { SYSTEM_TAB_ICONS, SYSTEM_TAB_LABELS } from './systemTabs';
import type { FormTab } from './types';
interface TaskTabsBarProps {
isMobile: boolean;
chatEnabled: boolean;
chatLayout: 'left' | 'tab';
expandedPane: 'none' | 'chat' | 'task';
onToggleExpand: (pane: 'chat' | 'task') => void;
formTabs: FormTab[];
orderedSystemTabs: string[];
subtasksCount: number;
selectedTabId: string;
onSelectTab: (value: string) => void;
isTaskFinal: boolean;
tabsScrollRef: (el: HTMLDivElement | null) => void;
tabsScrollEl: HTMLDivElement | null;
showLeftArrow: boolean;
showRightArrow: boolean;
}
export function TaskTabsBar({
isMobile,
chatEnabled,
chatLayout,
expandedPane,
onToggleExpand: toggleExpand,
formTabs,
orderedSystemTabs,
subtasksCount,
selectedTabId,
onSelectTab: setSelectedTabId,
isTaskFinal,
tabsScrollRef,
tabsScrollEl,
showLeftArrow,
showRightArrow,
}: TaskTabsBarProps) {
// Кнопка вкладки
const TabBtn = ({ value, icon: Icon, children, count }: {
value: string;
icon?: React.ElementType;
children: React.ReactNode;
count?: number;
}) => (
<button
onClick={() => setSelectedTabId(value)}
aria-current={selectedTabId === value ? 'page' : undefined}
data-testid={`tab-btn-${value}`}
className={`whitespace-nowrap text-xs px-3 py-2 border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
selectedTabId === value
? 'border-primary text-foreground font-medium'
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground/30'
}`}
>
{Icon && <Icon className="w-3 h-3" />}
<span className={isTaskFinal ? 'line-through' : ''}>{children}</span>
{count !== undefined && (
<span className="text-[11px] bg-muted rounded-full px-1 leading-4">{count}</span>
)}
</button>
);
return (
<>
{/* Горизонтальная полоса вкладок — десктоп */}
{!isMobile && (
<div className="border-b bg-background flex items-stretch overflow-hidden shrink-0">
{/* Чат слева — метка в левой секции таббара (десктоп) */}
{chatLayout === 'left' && chatEnabled && expandedPane !== 'task' && (
<div className="w-1/3 border-r px-3 flex items-center justify-between shrink-0">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Чат</span>
<button
onClick={() => toggleExpand('chat')}
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
title={expandedPane === 'chat' ? 'Свернуть чат' : 'Развернуть чат'}
>
{expandedPane === 'chat' ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
</button>
</div>
)}
{/* Кнопка разворота задачи — перед вкладками (только на десктопе с чатом) */}
{chatLayout === 'left' && chatEnabled && expandedPane !== 'chat' && (
<button
onClick={() => toggleExpand('task')}
className="px-2 flex items-center text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
title={expandedPane === 'task' ? 'Свернуть задачу' : 'Развернуть задачу'}
>
{expandedPane === 'task' ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
</button>
)}
{/* Кнопки вкладок */}
{expandedPane !== 'chat' && (
<div className="relative flex-1 overflow-hidden">
{showLeftArrow && (
<button
className="absolute left-0 top-0 z-10 w-6 h-full bg-background flex items-center justify-center border-r shadow-sm text-muted-foreground hover:text-foreground transition-colors"
onClick={() => tabsScrollEl?.scrollBy({ left: -160, behavior: 'smooth' })}
>
<ChevronLeft className="w-3 h-3" />
</button>
)}
<div
ref={tabsScrollRef}
className="overflow-x-auto [&::-webkit-scrollbar]:hidden h-full"
style={{ scrollbarWidth: 'none' }}
>
<div className="flex items-stretch h-full">
{formTabs.map(tab => (
<TabBtn key={tab.id} value={tab.id.toString()}>{tab.name}</TabBtn>
))}
{orderedSystemTabs.map(st => (
<TabBtn
key={st}
value={st}
icon={SYSTEM_TAB_ICONS[st]}
count={st === 'subtasks' ? subtasksCount : undefined}
>
{SYSTEM_TAB_LABELS[st]}
</TabBtn>
))}
</div>
</div>
{showRightArrow && (
<button
className="absolute right-0 top-0 z-10 w-6 h-full bg-background flex items-center justify-center border-l shadow-sm text-muted-foreground hover:text-foreground transition-colors"
onClick={() => tabsScrollEl?.scrollBy({ left: 160, behavior: 'smooth' })}
>
<ChevronRight className="w-3 h-3" />
</button>
)}
</div>
)}
</div>
)}
{/* Мобильный таб-бар — только Задача / Чат */}
{isMobile && chatEnabled && (
<div className="border-b bg-background flex items-stretch overflow-hidden shrink-0">
<button
onClick={() => setSelectedTabId('task')}
className={`flex-1 text-xs px-3 py-2 border-b-2 flex items-center justify-center gap-1.5 transition-colors ${
selectedTabId !== 'chat'
? 'border-primary text-foreground font-medium'
: 'border-transparent text-muted-foreground'
}`}
>
<FileText className="w-3.5 h-3.5" />
Задача
</button>
<button
onClick={() => setSelectedTabId('chat')}
className={`flex-1 text-xs px-3 py-2 border-b-2 flex items-center justify-center gap-1.5 transition-colors ${
selectedTabId === 'chat'
? 'border-primary text-foreground font-medium'
: 'border-transparent text-muted-foreground'
}`}
>
<MessageSquare className="w-3.5 h-3.5" />
Чат
</button>
</div>
)}
</>
);
}