mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
fix(kanban-card): jumping hover animation & drag overlay consistency
This commit is contained in:
@@ -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<string, string>).borderWidth = '0px';
|
||||
(borderStyle as Record<string, string>).borderColor = 'transparent';
|
||||
} else if (cardBorderOpacity !== 100) {
|
||||
(borderStyle as Record<string, string>).borderWidth = '1px';
|
||||
(borderStyle as Record<string, string>).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 = () => (
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={isCurrentAutoTask ? style : borderStyle}
|
||||
className={cn(
|
||||
'cursor-grab active:cursor-grabbing relative kanban-card-content select-none',
|
||||
'transition-all duration-200 ease-out',
|
||||
// Premium shadow system
|
||||
'shadow-sm hover:shadow-md hover:shadow-black/10',
|
||||
// Subtle lift on hover
|
||||
'hover:-translate-y-0.5',
|
||||
!isCurrentAutoTask && cardBorderEnabled && cardBorderOpacity === 100 && 'border-border/50',
|
||||
!isCurrentAutoTask && cardBorderEnabled && cardBorderOpacity !== 100 && 'border',
|
||||
!isDragging && 'bg-transparent',
|
||||
!glassmorphism && 'backdrop-blur-[0px]!',
|
||||
isDragging && 'scale-105 shadow-xl shadow-black/20 rotate-1',
|
||||
// Error state - using CSS variable
|
||||
feature.error &&
|
||||
!isCurrentAutoTask &&
|
||||
'border-[var(--status-error)] border-2 shadow-[var(--status-error-bg)] shadow-lg',
|
||||
!isDraggable && 'cursor-default'
|
||||
)}
|
||||
data-testid={`kanban-card-${feature.id}`}
|
||||
style={isCurrentAutoTask ? undefined : cardStyle}
|
||||
className={innerCardClasses}
|
||||
onDoubleClick={onEdit}
|
||||
{...attributes}
|
||||
{...(isDraggable ? listeners : {})}
|
||||
>
|
||||
{/* Background overlay with opacity */}
|
||||
{!isDragging && (
|
||||
{(!isDragging || isOverlay) && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 rounded-xl bg-card -z-10',
|
||||
@@ -185,10 +206,20 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</Card>
|
||||
);
|
||||
|
||||
// Wrap with animated border when in progress
|
||||
if (isCurrentAutoTask) {
|
||||
return <div className="animated-border-wrapper">{cardElement}</div>;
|
||||
}
|
||||
|
||||
return cardElement;
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={dndStyle}
|
||||
{...attributes}
|
||||
{...(isDraggable ? listeners : {})}
|
||||
className={wrapperClasses}
|
||||
data-testid={`kanban-card-${feature.id}`}
|
||||
>
|
||||
{isCurrentAutoTask ? (
|
||||
<div className="animated-border-wrapper">{renderCardContent()}</div>
|
||||
) : (
|
||||
renderCardContent()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 && (
|
||||
<Card
|
||||
className="rotate-2 shadow-2xl shadow-black/25 border-primary/50 bg-card/95 backdrop-blur-sm transition-transform"
|
||||
style={{ width: `${columnWidth}px` }}
|
||||
>
|
||||
<CardHeader className="p-3">
|
||||
<CardTitle className="text-sm font-medium line-clamp-2">
|
||||
{activeFeature.description}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground">
|
||||
{activeFeature.category}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div style={{ width: `${columnWidth}px` }}>
|
||||
<KanbanCard
|
||||
feature={activeFeature}
|
||||
isOverlay
|
||||
onEdit={() => {}}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
Reference in New Issue
Block a user