mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-06 19:57:50 +00:00
Merge pull request #1274 from BradGroux/fix/intentional-motion-contract-1259
fix: constrain shared interface motion
This commit is contained in:
commit
676f2d98f5
15 changed files with 66 additions and 44 deletions
|
|
@ -12,12 +12,33 @@ vi.mock('@/components/dashboard/Dashboard', () => ({
|
|||
|
||||
const motionSources = [
|
||||
'components/activity/ActivityFeed.tsx',
|
||||
'components/board/BoardSidebar.tsx',
|
||||
'components/board/KanbanColumn.tsx',
|
||||
'components/board/MultiAgentPanel.tsx',
|
||||
'components/chat/FloatingChat.tsx',
|
||||
'components/dashboard/Dashboard.tsx',
|
||||
'components/dashboard/DashboardPage.tsx',
|
||||
'components/dashboard/DashboardSection.tsx',
|
||||
'components/dashboard/HourlyActivityChart.tsx',
|
||||
'components/dashboard/StatusTimeline.tsx',
|
||||
'components/dashboard/WhereTimeWent.tsx',
|
||||
'components/layout/ActivitySidebar.tsx',
|
||||
'components/task/MultiAgentSelector.tsx',
|
||||
'components/task/TaskCard.tsx',
|
||||
'components/templates/TemplatesPage.tsx',
|
||||
'components/ui/badge.tsx',
|
||||
'components/ui/button.tsx',
|
||||
'components/ui/switch.tsx',
|
||||
'components/ui/tabs.tsx',
|
||||
];
|
||||
|
||||
const stableStatusSources = [
|
||||
'components/board/BoardSidebar.tsx',
|
||||
'components/board/MultiAgentPanel.tsx',
|
||||
'components/chat/FloatingChat.tsx',
|
||||
'components/task/AgentPanel.tsx',
|
||||
'components/task/TaskCard.tsx',
|
||||
'components/task/TimeTrackingSection.tsx',
|
||||
];
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
|
|
@ -53,6 +74,23 @@ describe('dashboard motion contract', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('keeps persistent live and unread status treatments stable', () => {
|
||||
for (const file of stableStatusSources) {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src', file), 'utf8');
|
||||
|
||||
expect(source, file).not.toContain('animate-ping');
|
||||
expect(source, file).not.toContain('animate-pulse');
|
||||
expect(source, file).not.toMatch(/hover:(?:scale|translate)/);
|
||||
}
|
||||
|
||||
const taskCard = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/task/TaskCard.tsx'),
|
||||
'utf8'
|
||||
);
|
||||
expect(taskCard).toContain('motion-safe:rotate-2');
|
||||
expect(taskCard).toContain('motion-safe:scale-105');
|
||||
});
|
||||
|
||||
it('expands and collapses immediately with explicit accessible state', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DashboardSection />);
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ function AgentStatusPanel({ onTaskClick }: { onTaskClick?: (taskId: string) => v
|
|||
{/* Status header — large icon + state */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 ring-2 transition-all"
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 ring-2 transition-colors"
|
||||
style={{ backgroundColor: config.bgColor, ['--tw-ring-color' as string]: config.color }}
|
||||
>
|
||||
<Icon className="w-5 h-5 transition-colors" style={{ color: config.color }} />
|
||||
|
|
@ -186,7 +186,7 @@ function AgentStatusPanel({ onTaskClick }: { onTaskClick?: (taskId: string) => v
|
|||
>
|
||||
{state !== 'idle' && (
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full animate-pulse"
|
||||
className="inline-block w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: config.color }}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -226,7 +226,7 @@ function AgentStatusPanel({ onTaskClick }: { onTaskClick?: (taskId: string) => v
|
|||
onClick={() => agent.taskId && onTaskClick?.(agent.taskId)}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full mt-1 shrink-0 animate-pulse"
|
||||
className="inline-block w-2 h-2 rounded-full mt-1 shrink-0"
|
||||
style={{ backgroundColor: agentState.color }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function KanbanColumn({
|
|||
aria-labelledby={`column-heading-${id}`}
|
||||
aria-roledescription="kanban column"
|
||||
className={cn(
|
||||
'flex flex-col rounded-lg bg-muted/50 border-t-2 transition-all',
|
||||
'flex flex-col rounded-lg bg-muted/50 border-t-2 transition-colors',
|
||||
columnColors[id] ?? 'border-t-primary',
|
||||
dragEnabled && isOver && 'ring-2 ring-primary/50 bg-muted/70'
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function AgentCard({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="rounded-md border transition-all"
|
||||
className="rounded-md border transition-colors"
|
||||
style={{ borderColor: `${style.color}30`, backgroundColor: style.bg }}
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -69,10 +69,7 @@ function AgentCard({
|
|||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{/* Status dot */}
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${isActive ? 'animate-pulse' : ''}`}
|
||||
style={{ backgroundColor: style.color }}
|
||||
/>
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: style.color }} />
|
||||
|
||||
{/* Name + model */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
|
|||
|
|
@ -58,16 +58,15 @@ export function FloatingChat() {
|
|||
className={cn(
|
||||
'floating-chat-trigger z-40 h-14 w-14 rounded-full shadow-lg',
|
||||
'bg-primary hover:bg-primary/90 text-primary-foreground',
|
||||
'transition-all duration-200 hover:scale-105',
|
||||
'transition-colors duration-150',
|
||||
open && 'hidden'
|
||||
)}
|
||||
aria-label="Open chat"
|
||||
>
|
||||
<MessageSquare className="h-6 w-6" />
|
||||
{hasUnread && (
|
||||
<span className="absolute -top-1 -right-1 flex h-4 w-4">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-4 w-4 bg-emerald-500" />
|
||||
<span className="absolute -top-1 -right-1 flex h-4 w-4" aria-hidden="true">
|
||||
<span className="inline-flex h-4 w-4 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
)}
|
||||
</ActionIcon>
|
||||
|
|
|
|||
|
|
@ -169,19 +169,10 @@ function DailySummaryCard() {
|
|||
{/* Mini progress bar */}
|
||||
{total > 0 && (
|
||||
<div className="mt-2 h-1.5 rounded-full overflow-hidden flex bg-muted">
|
||||
<div
|
||||
className="bg-green-500 transition-all"
|
||||
style={{ width: `${(summary.activeMs / total) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-gray-400 transition-all"
|
||||
style={{ width: `${(summary.idleMs / total) * 100}%` }}
|
||||
/>
|
||||
<div className="bg-green-500" style={{ width: `${(summary.activeMs / total) * 100}%` }} />
|
||||
<div className="bg-gray-400" style={{ width: `${(summary.idleMs / total) * 100}%` }} />
|
||||
{summary.errorMs > 0 && (
|
||||
<div
|
||||
className="bg-red-500 transition-all"
|
||||
style={{ width: `${(summary.errorMs / total) * 100}%` }}
|
||||
/>
|
||||
<div className="bg-red-500" style={{ width: `${(summary.errorMs / total) * 100}%` }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ export function AgentPanel({ task, onOpenTimeline }: AgentPanelProps) {
|
|||
)}
|
||||
{isAgentRunning && (
|
||||
<Text component="span" size="xs" c="green" className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="h-2 w-2 rounded-full bg-green-500" />
|
||||
Running
|
||||
</Text>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ export function MultiAgentSelector({
|
|||
const removeAgent = (agentId: string) => {
|
||||
const newAgents = agents.filter((a) => a !== agentId);
|
||||
// If we removed the primary, promote next
|
||||
const primary =
|
||||
primaryAgent === agentId ? newAgents[0] || undefined : primaryAgent;
|
||||
const primary = primaryAgent === agentId ? newAgents[0] || undefined : primaryAgent;
|
||||
onChange(newAgents, primary);
|
||||
};
|
||||
|
||||
|
|
@ -97,7 +96,7 @@ export function MultiAgentSelector({
|
|||
return (
|
||||
<span
|
||||
key={agent}
|
||||
className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-full cursor-pointer transition-all ${
|
||||
className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-full cursor-pointer transition-colors ${
|
||||
isPrimary ? 'ring-1' : ''
|
||||
}`}
|
||||
style={{
|
||||
|
|
@ -110,9 +109,7 @@ export function MultiAgentSelector({
|
|||
>
|
||||
<Bot className="w-3 h-3" />
|
||||
{agent}
|
||||
{isPrimary && (
|
||||
<span className="text-[8px] font-bold opacity-60">★</span>
|
||||
)}
|
||||
{isPrimary && <span className="text-[8px] font-bold opacity-60">★</span>}
|
||||
<button
|
||||
className="ml-0.5 hover:bg-white/20 rounded-full p-0.5 transition-colors"
|
||||
onClick={(e) => {
|
||||
|
|
@ -128,7 +125,7 @@ export function MultiAgentSelector({
|
|||
|
||||
{/* Add button */}
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground/60 hover:text-muted-foreground px-2 py-0.5 rounded-full border border-dashed border-muted-foreground/20 hover:border-muted-foreground/40 transition-all"
|
||||
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground/60 hover:text-muted-foreground px-2 py-0.5 rounded-full border border-dashed border-muted-foreground/20 hover:border-muted-foreground/40 transition-colors"
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
|
|
|
|||
|
|
@ -368,10 +368,10 @@ export const TaskCard = memo(function TaskCard({
|
|||
'group bg-card border border-border rounded-md',
|
||||
dragEnabled ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer',
|
||||
isCompact ? 'p-2' : 'p-3',
|
||||
'hover:border-muted-foreground/50 hover:bg-card/80 transition-all',
|
||||
'hover:border-muted-foreground/50 hover:bg-card/80 transition-colors',
|
||||
'border-l-2',
|
||||
typeColor,
|
||||
isDragging && 'opacity-50 shadow-lg rotate-2 scale-105',
|
||||
isDragging && 'opacity-50 shadow-lg motion-safe:rotate-2 motion-safe:scale-105',
|
||||
isCurrentlyDragging && 'opacity-50',
|
||||
isSelected && 'ring-2 ring-primary border-primary',
|
||||
isAgentRunning &&
|
||||
|
|
@ -423,7 +423,7 @@ export const TaskCard = memo(function TaskCard({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 flex items-center gap-1 animate-pulse">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 flex items-center gap-1">
|
||||
<span className="sr-only">
|
||||
Agent {agentNames[task.attempt?.agent || ''] || task.attempt?.agent} is actively
|
||||
running on this task
|
||||
|
|
@ -666,7 +666,7 @@ export const TaskCard = memo(function TaskCard({
|
|||
)}
|
||||
>
|
||||
{task.timeTracking?.isRunning ? (
|
||||
<Timer className="h-3 w-3 animate-pulse" />
|
||||
<Timer className="h-3 w-3" />
|
||||
) : (
|
||||
<Clock className="h-3 w-3" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export function TimeTrackingSection({ task }: TimeTrackingSectionProps) {
|
|||
|
||||
{isRunning && activeEntry && (
|
||||
<Group gap="xs">
|
||||
<Timer className="h-4 w-4 text-green-500 animate-pulse" />
|
||||
<Timer className="h-4 w-4 text-green-500" />
|
||||
<RunningTimer startTime={activeEntry.startTime} />
|
||||
</Group>
|
||||
)}
|
||||
|
|
@ -319,7 +319,7 @@ export function TimeTrackingSection({ task }: TimeTrackingSectionProps) {
|
|||
<Box className="min-w-0 flex-1">
|
||||
<Group gap="xs">
|
||||
{isActive ? (
|
||||
<Timer className="h-3 w-3 text-green-500 animate-pulse flex-shrink-0" />
|
||||
<Timer className="h-3 w-3 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<Clock className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export function TemplatesPage({ onBack }: TemplatesPageProps) {
|
|||
<div
|
||||
key={template.id}
|
||||
className={cn(
|
||||
'p-4 cursor-pointer transition-all border rounded-lg hover:border-primary hover:shadow-md',
|
||||
'p-4 cursor-pointer transition-colors border rounded-lg hover:border-primary hover:bg-muted/30',
|
||||
selectedTemplate?.id === template.id && 'border-primary bg-primary/5'
|
||||
)}
|
||||
onClick={() => setSelectedTemplate(template)}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { SlotRoot } from '@/components/ui/slot';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
|
||||
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { SlotRoot } from '@/components/ui/slot';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function Switch({ className, size = 'default', onCheckedChange, onChange, ...pro
|
|||
classNames={{
|
||||
root: 'inline-flex',
|
||||
track: cn(
|
||||
'cursor-pointer border-transparent transition-all focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50'
|
||||
'cursor-pointer border-transparent transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50'
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Mantin
|
|||
<MantineTabs.Tab
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-colors group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
||||
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
|
||||
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue