diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx index dd21f286..1226d9b3 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useLayoutEffect, useState } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { cn } from '@/lib/utils'; @@ -10,6 +10,25 @@ import { CardContentSections } from './card-content-sections'; import { AgentInfoPanel } from './agent-info-panel'; import { CardActions } from './card-actions'; +function getCardBorderStyle(enabled: boolean, opacity: number): React.CSSProperties { + if (!enabled) { + return { borderWidth: '0px', borderColor: 'transparent' }; + } + if (opacity !== 100) { + return { + borderWidth: '1px', + borderColor: `color-mix(in oklch, var(--border) ${opacity}%, transparent)`, + }; + } + return {}; +} + +function getCursorClass(isOverlay: boolean | undefined, isDraggable: boolean): string { + if (isOverlay) return 'cursor-grabbing'; + if (isDraggable) return 'cursor-grab active:cursor-grabbing'; + return 'cursor-default'; +} + interface KanbanCardProps { feature: Feature; onEdit: () => void; @@ -35,6 +54,7 @@ interface KanbanCardProps { glassmorphism?: boolean; cardBorderEnabled?: boolean; cardBorderOpacity?: number; + isOverlay?: boolean; } export const KanbanCard = memo(function KanbanCard({ @@ -62,8 +82,18 @@ export const KanbanCard = memo(function KanbanCard({ glassmorphism = true, cardBorderEnabled = true, cardBorderOpacity = 100, + isOverlay, }: KanbanCardProps) { const { useWorktrees } = useAppStore(); + const [isLifted, setIsLifted] = useState(false); + + useLayoutEffect(() => { + if (isOverlay) { + requestAnimationFrame(() => { + setIsLifted(true); + }); + } + }, [isOverlay]); const isDraggable = feature.status === 'backlog' || @@ -72,54 +102,45 @@ export const KanbanCard = memo(function KanbanCard({ (feature.status === 'in_progress' && !isCurrentAutoTask); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: feature.id, - disabled: !isDraggable, + disabled: !isDraggable || isOverlay, }); - const style = { + const dndStyle = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : undefined, }; - const borderStyle: React.CSSProperties = { ...style }; - if (!cardBorderEnabled) { - (borderStyle as Record).borderWidth = '0px'; - (borderStyle as Record).borderColor = 'transparent'; - } else if (cardBorderOpacity !== 100) { - (borderStyle as Record).borderWidth = '1px'; - (borderStyle as Record).borderColor = - `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; - } + const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity); - const cardElement = ( + const wrapperClasses = cn( + 'relative select-none outline-none touch-none', + getCursorClass(isOverlay, isDraggable), + isOverlay && isLifted && 'scale-105 rotate-1 z-50' + ); + + const isInteractive = !isDragging && !isOverlay; + const hasError = feature.error && !isCurrentAutoTask; + + const innerCardClasses = cn( + 'kanban-card-content h-full relative shadow-sm', + 'transition-all duration-200 ease-out', + isInteractive && 'hover:-translate-y-0.5 hover:shadow-md hover:shadow-black/10 bg-transparent', + !glassmorphism && 'backdrop-blur-[0px]!', + !isCurrentAutoTask && + cardBorderEnabled && + (cardBorderOpacity === 100 ? 'border-border/50' : 'border'), + hasError && 'border-[var(--status-error)] border-2 shadow-[var(--status-error-bg)] shadow-lg' + ); + + const renderCardContent = () => ( {/* Background overlay with opacity */} - {!isDragging && ( + {(!isDragging || isOverlay) && (
); - // Wrap with animated border when in progress - if (isCurrentAutoTask) { - return
{cardElement}
; - } - - return cardElement; + return ( +
+ {isCurrentAutoTask ? ( +
{renderCardContent()}
+ ) : ( + renderCardContent() + )} +
+ ); }); diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx index db3270ea..eecadc61 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import { DndContext, DragOverlay } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { KanbanColumn, KanbanCard } from './components'; @@ -241,19 +240,32 @@ export function KanbanBoard({ }} > {activeFeature && ( - - - - {activeFeature.description} - - - {activeFeature.category} - - - +
+ {}} + onDelete={() => {}} + onViewOutput={() => {}} + onVerify={() => {}} + onResume={() => {}} + onForceStop={() => {}} + onManualVerify={() => {}} + onMoveBackToInProgress={() => {}} + onFollowUp={() => {}} + onImplement={() => {}} + onComplete={() => {}} + onViewPlan={() => {}} + onApprovePlan={() => {}} + onSpawnTask={() => {}} + hasContext={featuresWithContext.has(activeFeature.id)} + isCurrentAutoTask={runningAutoTasks.includes(activeFeature.id)} + opacity={backgroundSettings.cardOpacity} + glassmorphism={backgroundSettings.cardGlassmorphism} + cardBorderEnabled={backgroundSettings.cardBorderEnabled} + cardBorderOpacity={backgroundSettings.cardBorderOpacity} + /> +
)}