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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user