feat(floating-ai): step 7 — implement morph animation (FLIP)

Add FLIP animation so the floating chat visually morphs into a newly-created
TaskRow when the AI creates a task. Uses Framer Motion's shared layoutId
across FloatingChat and TaskRow, with LayoutGroup wrapping the app shell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-02-28 13:27:23 +01:00
parent d12681b79f
commit 60b76c6d97
6 changed files with 56 additions and 10 deletions

View File

@@ -12,9 +12,11 @@ import {
import { useAIChat, type ChatContext } from '@/hooks/useAIChat';
import { ChatMarkdown } from '@/components/ai/AIChatPanel';
import { Skeleton } from '@/components/ui/skeleton';
import { trpc } from '@/lib/trpc';
function FloatingChatInner() {
const { state, sections, close } = useFloatingChat();
const { state, sections, close, setMorphTarget } = useFloatingChat();
const utils = trpc.useUtils();
const routerState = useRouterState();
const prevPathRef = useRef(routerState.location.pathname);
@@ -77,6 +79,31 @@ function FloatingChatInner() {
prevOpenRef.current = state.isOpen;
}, [state.isOpen, clearMessages]);
// ---- AI action: morph into newly-created task ----
useEffect(() => {
if (!state.isOpen) return;
const unsubscribe = window.electronAI.onAction((action) => {
if (action.type === 'task_created' && action.taskId) {
// Invalidate task queries so the new TaskRow renders
void utils.tasks.list.invalidate();
// Set the morph target layoutId
setMorphTarget(`task-morph-${action.taskId}`);
// Wait for the TaskRow to render, then close (triggering FLIP)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
close();
});
});
}
});
return unsubscribe;
}, [state.isOpen, utils, setMorphTarget, close]);
// ---- Window resize: keep within bounds ----
useEffect(() => {