feat(task-management): enhance task row and virtualized list components for improved layout and performance

- Added support for customizable columns in `TaskRow` component, allowing for fixed and scrollable columns.
- Implemented synchronized scrolling between header and body in `VirtualizedTaskList` for better user experience.
- Refactored column header rendering to dynamically generate based on column definitions, improving maintainability.
- Enhanced styles for task group headers and column headers to ensure consistent appearance and responsiveness.
This commit is contained in:
chamikaJ
2025-06-23 16:49:57 +05:30
parent 2dd756bbb8
commit 95d0985f3d
2 changed files with 370 additions and 344 deletions

View File

@@ -26,6 +26,9 @@ interface TaskRowProps {
index?: number; index?: number;
onSelect?: (taskId: string, selected: boolean) => void; onSelect?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void; onToggleSubtasks?: (taskId: string) => void;
columns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>;
fixedColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>;
scrollableColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>;
} }
// Priority and status colors - moved outside component to avoid recreation // Priority and status colors - moved outside component to avoid recreation
@@ -52,6 +55,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
index, index,
onSelect, onSelect,
onToggleSubtasks, onToggleSubtasks,
columns,
fixedColumns,
scrollableColumns,
}) => { }) => {
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
@@ -217,189 +223,222 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
> >
<div className="flex h-10 max-h-10 overflow-visible relative min-w-[1200px]"> <div className="flex h-10 max-h-10 overflow-visible relative min-w-[1200px]">
{/* Fixed Columns */} {/* Fixed Columns */}
<div className={fixedColumnsClasses}> <div
{/* Drag Handle */} className="fixed-columns-row"
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> style={{
<Button display: 'flex',
variant="text" position: 'sticky',
size="small" left: 0,
icon={<HolderOutlined />} zIndex: 2,
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing" background: isDarkMode ? '#1a1a1a' : '#fff',
isDarkMode={isDarkMode} width: fixedColumns?.reduce((sum, col) => sum + col.width, 0) || 0,
{...attributes} }}
{...listeners} >
/> {fixedColumns?.map(col => {
</div> switch (col.key) {
case 'drag':
{/* Selection Checkbox */} return (
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> <div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
<Checkbox <Button
checked={isSelected} variant="text"
onChange={handleSelectChange} size="small"
isDarkMode={isDarkMode} icon={<HolderOutlined />}
/> className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
</div> isDarkMode={isDarkMode}
{...attributes}
{/* Task Key */} {...listeners}
<div className={`w-20 flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
<Tag
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
color={isDarkMode ? "#d1d5db" : "#666"}
className="truncate whitespace-nowrap max-w-full"
>
{task.task_key}
</Tag>
</div>
{/* Task Name */}
<div className={`w-[475px] flex items-center px-2 ${editTaskName ? (isDarkMode ? 'bg-blue-900/10 border border-blue-500' : 'bg-blue-50/20 border border-blue-500') : ''}`}>
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
<div className="flex items-center gap-2 h-5 overflow-hidden">
<div ref={wrapperRef} className="flex-1 min-w-0">
{!editTaskName ? (
<Typography.Text
ellipsis={{ tooltip: task.title }}
onClick={() => setEditTaskName(true)}
className={taskNameClasses}
style={{ cursor: 'pointer' }}
>
{task.title}
</Typography.Text>
) : (
<Input
ref={inputRef}
variant="borderless"
value={taskName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
onPressEnter={handleTaskNameSave}
className={`${isDarkMode ? 'bg-gray-800 text-gray-100 border-gray-600' : 'bg-white text-gray-900 border-gray-300'}`}
style={{
width: '100%',
padding: '2px 4px',
fontSize: '14px',
fontWeight: 500,
}}
/> />
)} </div>
</div> );
</div> case 'select':
</div> return (
</div> <div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
<Checkbox
checked={isSelected}
onChange={handleSelectChange}
isDarkMode={isDarkMode}
/>
</div>
);
case 'key':
return (
<div key={col.key} className="w-20 flex items-center px-2 border-r" style={{ width: col.width }}>
<Tag
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
color={isDarkMode ? "#d1d5db" : "#666"}
className="truncate whitespace-nowrap max-w-full"
>
{task.task_key}
</Tag>
</div>
);
case 'task':
return (
<div key={col.key} className="flex items-center px-2" style={{ width: col.width }}>
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
<div className="flex items-center gap-2 h-5 overflow-hidden">
<div ref={wrapperRef} className="flex-1 min-w-0">
{!editTaskName ? (
<Typography.Text
ellipsis={{ tooltip: task.title }}
onClick={() => setEditTaskName(true)}
className={taskNameClasses}
style={{ cursor: 'pointer' }}
>
{task.title}
</Typography.Text>
) : (
<Input
ref={inputRef}
variant="borderless"
value={taskName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
onPressEnter={handleTaskNameSave}
className={`${isDarkMode ? 'bg-gray-800 text-gray-100 border-gray-600' : 'bg-white text-gray-900 border-gray-300'}`}
style={{
width: '100%',
padding: '2px 4px',
fontSize: '14px',
fontWeight: 500,
}}
/>
)}
</div>
</div>
</div>
</div>
);
default:
return null;
}
})}
</div> </div>
{/* Scrollable Columns */} {/* Scrollable Columns */}
<div className="flex flex-1 min-w-0"> <div className="scrollable-columns-row" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
{/* Progress */} {scrollableColumns?.map(col => {
<div className={`w-[90px] flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> switch (col.key) {
{task.progress !== undefined && task.progress >= 0 && ( case 'progress':
<Progress return (
type="circle" <div key={col.key} className="flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
percent={task.progress} {task.progress !== undefined && task.progress >= 0 && (
size={24} <Progress
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'} type="circle"
strokeWidth={2} percent={task.progress}
showInfo={true} size={24}
isDarkMode={isDarkMode} strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
/> strokeWidth={2}
)} showInfo={true}
</div> isDarkMode={isDarkMode}
/>
{/* Members */} )}
<div className={`w-[150px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> </div>
<div className="flex items-center gap-2"> );
{avatarGroupMembers.length > 0 && ( case 'members':
<AvatarGroup return (
members={avatarGroupMembers} <div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
size={24} <div className="flex items-center gap-2">
maxCount={3} {avatarGroupMembers.length > 0 && (
isDarkMode={isDarkMode} <AvatarGroup
/> members={avatarGroupMembers}
)} size={24}
<button maxCount={3}
className={` isDarkMode={isDarkMode}
w-6 h-6 rounded-full border border-dashed flex items-center justify-center />
transition-colors duration-200 )}
${isDarkMode <button
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400' className={`
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600' w-6 h-6 rounded-full border border-dashed flex items-center justify-center
} transition-colors duration-200
`} ${isDarkMode
onClick={() => { ? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
// TODO: Implement assignee selector functionality : 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
console.log('Add assignee clicked for task:', task.id); }
}} `}
> onClick={() => {
<span className="text-xs">+</span> // TODO: Implement assignee selector functionality
</button> console.log('Add assignee clicked for task:', task.id);
</div> }}
</div> >
<span className="text-xs">+</span>
{/* Labels */} </button>
<div className={`w-[200px] max-w-[200px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> </div>
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative"> </div>
{task.labels?.map((label, index) => ( );
label.end && label.names && label.name ? ( case 'labels':
<CustomNumberLabel return (
key={`${label.id}-${index}`} <div key={col.key} className="max-w-[200px] flex items-center px-2 border-r" style={{ width: col.width }}>
labelList={label.names} <div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
namesString={label.name} {task.labels?.map((label, index) => (
isDarkMode={isDarkMode} label.end && label.names && label.name ? (
/> <CustomNumberLabel
) : ( key={`${label.id}-${index}`}
<CustomColordLabel labelList={label.names}
key={`${label.id}-${index}`} namesString={label.name}
label={label} isDarkMode={isDarkMode}
isDarkMode={isDarkMode} />
/> ) : (
) <CustomColordLabel
))} key={`${label.id}-${index}`}
<LabelsSelector label={label}
task={taskAdapter} isDarkMode={isDarkMode}
isDarkMode={isDarkMode} />
/> )
</div> ))}
</div> <LabelsSelector
task={taskAdapter}
{/* Status */} isDarkMode={isDarkMode}
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> />
<Tag </div>
backgroundColor={getStatusColor(task.status)} </div>
color="white" );
className="text-xs font-medium uppercase" case 'status':
> return (
{task.status} <div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
</Tag> <Tag
</div> backgroundColor={getStatusColor(task.status)}
color="white"
{/* Priority */} className="text-xs font-medium uppercase"
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> >
<div className="flex items-center gap-2"> {task.status}
<div </Tag>
className="w-2 h-2 rounded-full" </div>
style={{ backgroundColor: getPriorityColor(task.priority) }} );
/> case 'priority':
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}> return (
{task.priority} <div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
</span> <div className="flex items-center gap-2">
</div> <div
</div> className="w-2 h-2 rounded-full"
style={{ backgroundColor: getPriorityColor(task.priority) }}
{/* Time Tracking */} />
<div className={`w-[120px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}> <span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
<div className="flex items-center gap-2 h-full overflow-hidden"> {task.priority}
{task.timeTracking?.logged && task.timeTracking.logged > 0 && ( </span>
<div className="flex items-center gap-1"> </div>
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} /> </div>
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}> );
{typeof task.timeTracking.logged === 'number' case 'timeTracking':
? `${task.timeTracking.logged}h` return (
: task.timeTracking.logged <div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
} <div className="flex items-center gap-2 h-full overflow-hidden">
</span> {task.timeTracking?.logged && task.timeTracking.logged > 0 && (
</div> <div className="flex items-center gap-1">
)} <ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
</div> <span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
</div> {typeof task.timeTracking.logged === 'number'
? `${task.timeTracking.logged}h`
: task.timeTracking.logged
}
</span>
</div>
)}
</div>
</div>
);
default:
return null;
}
})}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useEffect } from 'react'; import React, { useMemo, useCallback, useEffect, useRef } from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@@ -64,120 +64,146 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
}); });
}, [group.id, groupTasks.length, height, listHeight]); }, [group.id, groupTasks.length, height, listHeight]);
// Row renderer for virtualization const scrollContainerRef = useRef<HTMLDivElement>(null);
const headerScrollRef = useRef<HTMLDivElement>(null);
// Synchronize header scroll with body scroll
useEffect(() => {
const handleScroll = () => {
if (headerScrollRef.current && scrollContainerRef.current) {
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
}
};
const scrollDiv = scrollContainerRef.current;
if (scrollDiv) {
scrollDiv.addEventListener('scroll', handleScroll);
}
return () => {
if (scrollDiv) {
scrollDiv.removeEventListener('scroll', handleScroll);
}
};
}, []);
// Define columns array for alignment
const columns = [
{ key: 'drag', label: '', width: 40, fixed: true },
{ key: 'select', label: '', width: 40, fixed: true },
{ key: 'key', label: 'KEY', width: 80, fixed: true },
{ key: 'task', label: 'TASK', width: 475, fixed: true },
{ key: 'progress', label: 'PROGRESS', width: 90 },
{ key: 'members', label: 'MEMBERS', width: 150 },
{ key: 'labels', label: 'LABELS', width: 200 },
{ key: 'status', label: 'STATUS', width: 100 },
{ key: 'priority', label: 'PRIORITY', width: 100 },
{ key: 'timeTracking', label: 'TIME TRACKING', width: 120 },
];
const fixedColumns = columns.filter(col => col.fixed);
const scrollableColumns = columns.filter(col => !col.fixed);
const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0);
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
const totalTableWidth = fixedWidth + scrollableWidth;
// Row renderer for virtualization (remove header/column header rows)
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => { const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
// Header row const task = groupTasks[index];
if (index === 0) { if (!task) return null;
return ( return (
<div style={style}> <div
<div className="task-group-header"> className="task-row-container"
<div className="task-group-header-row"> style={{
<div ...style,
className="task-group-header-content" '--group-color': group.color || '#f0f0f0'
style={{ } as React.CSSProperties}
backgroundColor: group.color || '#f0f0f0', >
borderLeft: `4px solid ${group.color || '#f0f0f0'}` <TaskRow
}} task={task}
> projectId={projectId}
<span className="task-group-header-text"> groupId={group.id}
{group.title} ({groupTasks.length}) currentGrouping={currentGrouping}
</span> isSelected={selectedTaskIds.includes(task.id)}
</div> index={index}
</div> onSelect={onSelectTask}
</div> onToggleSubtasks={onToggleSubtasks}
</div> fixedColumns={fixedColumns}
); scrollableColumns={scrollableColumns}
} />
</div>
// Column headers row );
if (index === 1) {
return (
<div style={style}>
<div
className="task-group-column-headers"
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}` }}
>
<div className="task-group-column-headers-row">
<div className="task-table-fixed-columns">
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
<div className="task-table-cell task-table-header-cell" style={{ width: '80px' }}>
<span className="column-header-text">Key</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '475px' }}>
<span className="column-header-text">Task</span>
</div>
</div>
<div className="task-table-scrollable-columns">
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
<span className="column-header-text">Progress</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
<span className="column-header-text">Members</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '200px' }}>
<span className="column-header-text">Labels</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<span className="column-header-text">Status</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<span className="column-header-text">Priority</span>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
<span className="column-header-text">Time Tracking</span>
</div>
</div>
</div>
</div>
</div>
);
}
// Task rows
const taskIndex = index - 2;
if (taskIndex >= 0 && taskIndex < groupTasks.length) {
const task = groupTasks[taskIndex];
return (
<div
className="task-row-container"
style={{
...style,
'--group-color': group.color || '#f0f0f0'
} as React.CSSProperties}
>
<TaskRow
task={task}
projectId={projectId}
groupId={group.id}
currentGrouping={currentGrouping}
isSelected={selectedTaskIds.includes(task.id)}
index={taskIndex}
onSelect={onSelectTask}
onToggleSubtasks={onToggleSubtasks}
/>
</div>
);
}
return null;
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]); }, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]);
return ( return (
<div className="virtualized-task-list" style={{ height: height }}> <div className="virtualized-task-list" style={{ height: height }}>
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}> {/* Group Header */}
<List <div className="task-group-header">
height={listHeight} <div className="task-group-header-row">
width={width} <div
itemCount={getItemCount()} className="task-group-header-content"
itemSize={TASK_ROW_HEIGHT} style={{
overscanCount={15} // Render 15 extra items for smooth scrolling backgroundColor: group.color || '#f0f0f0',
className="react-window-list" borderLeft: `4px solid ${group.color || '#f0f0f0'}`
}}
>
<span className="task-group-header-text">
{group.title} ({groupTasks.length})
</span>
</div>
</div>
</div>
{/* Column Headers (sync scroll) */}
<div
className="task-group-column-headers-scroll"
ref={headerScrollRef}
style={{ overflowX: 'auto', overflowY: 'hidden' }}
>
<div
className="task-group-column-headers"
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}`, minWidth: totalTableWidth, display: 'flex', position: 'relative' }}
> >
{Row} <div className="fixed-columns-header" style={{ display: 'flex', position: 'sticky', left: 0, zIndex: 2, background: 'inherit', width: fixedWidth }}>
</List> {fixedColumns.map(col => (
</SortableContext> <div
key={col.key}
className="task-table-cell task-table-header-cell fixed-column"
style={{ width: col.width }}
>
<span className="column-header-text">{col.label}</span>
</div>
))}
</div>
<div className="scrollable-columns-header" style={{ display: 'flex', minWidth: scrollableWidth }}>
{scrollableColumns.map(col => (
<div
key={col.key}
className="task-table-cell task-table-header-cell"
style={{ width: col.width }}
>
<span className="column-header-text">{col.label}</span>
</div>
))}
</div>
</div>
</div>
{/* Scrollable List */}
<div
className="task-list-scroll-container"
ref={scrollContainerRef}
style={{ overflowX: 'auto', overflowY: 'hidden', width: '100%', minWidth: totalTableWidth }}
>
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
<List
height={listHeight}
width={width}
itemCount={groupTasks.length}
itemSize={TASK_ROW_HEIGHT}
overscanCount={15}
className="react-window-list"
style={{ minWidth: totalTableWidth }}
>
{Row}
</List>
</SortableContext>
</div>
{/* Add Task Row - Always show at the bottom */} {/* Add Task Row - Always show at the bottom */}
<div <div
className="task-group-add-task" className="task-group-add-task"
@@ -185,7 +211,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
> >
<AddTaskListRow groupId={group.id} /> <AddTaskListRow groupId={group.id} />
</div> </div>
<style>{` <style>{`
.virtualized-task-list { .virtualized-task-list {
border: 1px solid var(--task-border-primary, #e8e8e8); border: 1px solid var(--task-border-primary, #e8e8e8);
@@ -199,11 +224,23 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.task-group-header {
.virtualized-task-list:last-child { position: relative;
margin-bottom: 0; z-index: 20;
}
.task-group-column-headers-scroll {
width: 100%;
}
.task-group-column-headers {
background: var(--task-bg-secondary, #f5f5f5);
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
margin: 0;
padding: 0;
min-width: 1200px;
}
.task-list-scroll-container {
width: 100%;
} }
.react-window-list { .react-window-list {
outline: none; outline: none;
flex: 1; flex: 1;
@@ -211,19 +248,16 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.react-window-list-item { .react-window-list-item {
contain: layout style; contain: layout style;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
/* Task row container styles */ /* Task row container styles */
.task-row-container { .task-row-container {
position: relative; position: relative;
background: var(--task-bg-primary, white); background: var(--task-bg-primary, white);
} }
.task-row-container::before { .task-row-container::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -234,24 +268,12 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
background-color: var(--group-color, #f0f0f0); background-color: var(--group-color, #f0f0f0);
z-index: 10; z-index: 10;
} }
/* Ensure no gaps between list items */ /* Ensure no gaps between list items */
.react-window-list > div { .react-window-list > div {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
/* Task group header styles */ /* Task group header styles */
.task-group-header {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
margin: 0;
padding: 0;
position: sticky;
top: 0;
z-index: 20;
}
.task-group-header-row { .task-group-header-row {
display: inline-flex; display: inline-flex;
height: auto; height: auto;
@@ -260,7 +282,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.task-group-header-content { .task-group-header-content {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -273,37 +294,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
margin: 0; margin: 0;
border: none; border: none;
} }
.task-group-header-text { .task-group-header-text {
color: white !important; color: white !important;
font-size: 13px !important; font-size: 13px !important;
font-weight: 600 !important; font-weight: 600 !important;
margin: 0 !important; margin: 0 !important;
} }
/* Column headers styles */ /* Column headers styles */
.task-group-column-headers {
background: var(--task-bg-secondary, #f5f5f5);
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
transition: background-color 0.3s ease;
margin: 0;
padding: 0;
position: sticky;
top: 40px;
z-index: 19;
}
.task-group-column-headers-row {
display: flex;
height: 40px;
max-height: 40px;
overflow: visible;
position: relative;
min-width: 1200px;
margin: 0;
padding: 0;
}
.task-table-header-cell { .task-table-header-cell {
background: var(--task-bg-secondary, #f5f5f5); background: var(--task-bg-secondary, #f5f5f5);
font-weight: 600; font-weight: 600;
@@ -316,7 +313,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.column-header-text { .column-header-text {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -325,7 +321,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
letter-spacing: 0.5px; letter-spacing: 0.5px;
transition: color 0.3s ease; transition: color 0.3s ease;
} }
/* Add task row styles */ /* Add task row styles */
.task-group-add-task { .task-group-add-task {
background: var(--task-bg-primary, white); background: var(--task-bg-primary, white);
@@ -338,11 +333,9 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
} }
.task-group-add-task:hover { .task-group-add-task:hover {
background: var(--task-hover-bg, #fafafa); background: var(--task-hover-bg, #fafafa);
} }
.task-table-fixed-columns { .task-table-fixed-columns {
display: flex; display: flex;
background: var(--task-bg-secondary, #f5f5f5); background: var(--task-bg-secondary, #f5f5f5);
@@ -353,13 +346,11 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1); box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.task-table-scrollable-columns { .task-table-scrollable-columns {
display: flex; display: flex;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.task-table-cell { .task-table-cell {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -374,16 +365,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
color: var(--task-text-primary, #262626); color: var(--task-text-primary, #262626);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.task-table-cell:last-child { .task-table-cell:last-child {
border-right: none; border-right: none;
} }
/* Performance optimizations */ /* Performance optimizations */
.virtualized-task-list { .virtualized-task-list {
contain: layout style paint; contain: layout style paint;
} }
/* Dark mode support */ /* Dark mode support */
:root { :root {
--task-bg-primary: #ffffff; --task-bg-primary: #ffffff;
@@ -402,7 +390,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
--task-drag-over-bg: #f0f8ff; --task-drag-over-bg: #f0f8ff;
--task-drag-over-border: #40a9ff; --task-drag-over-border: #40a9ff;
} }
.dark .virtualized-task-list, .dark .virtualized-task-list,
[data-theme="dark"] .virtualized-task-list { [data-theme="dark"] .virtualized-task-list {
--task-bg-primary: #1f1f1f; --task-bg-primary: #1f1f1f;