mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +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 { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -10,6 +10,25 @@ import { CardContentSections } from './card-content-sections';
|
|||||||
import { AgentInfoPanel } from './agent-info-panel';
|
import { AgentInfoPanel } from './agent-info-panel';
|
||||||
import { CardActions } from './card-actions';
|
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 {
|
interface KanbanCardProps {
|
||||||
feature: Feature;
|
feature: Feature;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -35,6 +54,7 @@ interface KanbanCardProps {
|
|||||||
glassmorphism?: boolean;
|
glassmorphism?: boolean;
|
||||||
cardBorderEnabled?: boolean;
|
cardBorderEnabled?: boolean;
|
||||||
cardBorderOpacity?: number;
|
cardBorderOpacity?: number;
|
||||||
|
isOverlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanCard = memo(function KanbanCard({
|
export const KanbanCard = memo(function KanbanCard({
|
||||||
@@ -62,8 +82,18 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
glassmorphism = true,
|
glassmorphism = true,
|
||||||
cardBorderEnabled = true,
|
cardBorderEnabled = true,
|
||||||
cardBorderOpacity = 100,
|
cardBorderOpacity = 100,
|
||||||
|
isOverlay,
|
||||||
}: KanbanCardProps) {
|
}: KanbanCardProps) {
|
||||||
const { useWorktrees } = useAppStore();
|
const { useWorktrees } = useAppStore();
|
||||||
|
const [isLifted, setIsLifted] = useState(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (isOverlay) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsLifted(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOverlay]);
|
||||||
|
|
||||||
const isDraggable =
|
const isDraggable =
|
||||||
feature.status === 'backlog' ||
|
feature.status === 'backlog' ||
|
||||||
@@ -72,54 +102,45 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
(feature.status === 'in_progress' && !isCurrentAutoTask);
|
(feature.status === 'in_progress' && !isCurrentAutoTask);
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: feature.id,
|
id: feature.id,
|
||||||
disabled: !isDraggable,
|
disabled: !isDraggable || isOverlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = {
|
const dndStyle = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
opacity: isDragging ? 0.5 : undefined,
|
opacity: isDragging ? 0.5 : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const borderStyle: React.CSSProperties = { ...style };
|
const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity);
|
||||||
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 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
|
<Card
|
||||||
ref={setNodeRef}
|
style={isCurrentAutoTask ? undefined : cardStyle}
|
||||||
style={isCurrentAutoTask ? style : borderStyle}
|
className={innerCardClasses}
|
||||||
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}`}
|
|
||||||
onDoubleClick={onEdit}
|
onDoubleClick={onEdit}
|
||||||
{...attributes}
|
|
||||||
{...(isDraggable ? listeners : {})}
|
|
||||||
>
|
>
|
||||||
{/* Background overlay with opacity */}
|
{/* Background overlay with opacity */}
|
||||||
{!isDragging && (
|
{(!isDragging || isOverlay) && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 rounded-xl bg-card -z-10',
|
'absolute inset-0 rounded-xl bg-card -z-10',
|
||||||
@@ -185,10 +206,20 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wrap with animated border when in progress
|
return (
|
||||||
if (isCurrentAutoTask) {
|
<div
|
||||||
return <div className="animated-border-wrapper">{cardElement}</div>;
|
ref={setNodeRef}
|
||||||
}
|
style={dndStyle}
|
||||||
|
{...attributes}
|
||||||
return cardElement;
|
{...(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 { useMemo } from 'react';
|
||||||
import { DndContext, DragOverlay } from '@dnd-kit/core';
|
import { DndContext, DragOverlay } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||||
import { KanbanColumn, KanbanCard } from './components';
|
import { KanbanColumn, KanbanCard } from './components';
|
||||||
@@ -241,19 +240,32 @@ export function KanbanBoard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeFeature && (
|
{activeFeature && (
|
||||||
<Card
|
<div style={{ width: `${columnWidth}px` }}>
|
||||||
className="rotate-2 shadow-2xl shadow-black/25 border-primary/50 bg-card/95 backdrop-blur-sm transition-transform"
|
<KanbanCard
|
||||||
style={{ width: `${columnWidth}px` }}
|
feature={activeFeature}
|
||||||
>
|
isOverlay
|
||||||
<CardHeader className="p-3">
|
onEdit={() => {}}
|
||||||
<CardTitle className="text-sm font-medium line-clamp-2">
|
onDelete={() => {}}
|
||||||
{activeFeature.description}
|
onViewOutput={() => {}}
|
||||||
</CardTitle>
|
onVerify={() => {}}
|
||||||
<CardDescription className="text-xs text-muted-foreground">
|
onResume={() => {}}
|
||||||
{activeFeature.category}
|
onForceStop={() => {}}
|
||||||
</CardDescription>
|
onManualVerify={() => {}}
|
||||||
</CardHeader>
|
onMoveBackToInProgress={() => {}}
|
||||||
</Card>
|
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>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|||||||
Reference in New Issue
Block a user