diff --git a/app/globals.css b/app/globals.css index 0ab89a899..7b7d09eb7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -55,6 +55,9 @@ --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); --tracking-normal: 0em; --spacing: 0.25rem; + --motion-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --motion-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); + --motion-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); } .dark { @@ -161,13 +164,13 @@ --shadow-xl: var(--shadow-xl); --shadow-2xl: var(--shadow-2xl); - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-accordion-up: accordion-up 0.2s ease-out; - --animate-collapse-down: collapse-down 0.1s ease-in-out; - --animate-collapse-up: collapse-up 0.1s ease-in-out; - --animate-slide-in-right: slide-in-right 0.3s ease-out; - --animate-slide-out-right: slide-out-right 0.3s ease-out; - --animate-fade-in: fade-in 0.3s ease-out; + --animate-accordion-down: accordion-down 180ms var(--motion-ease-out); + --animate-accordion-up: accordion-up 140ms var(--motion-ease-out); + --animate-collapse-down: collapse-down 160ms var(--motion-ease-out); + --animate-collapse-up: collapse-up 120ms var(--motion-ease-out); + --animate-slide-in-right: slide-in-right 220ms var(--motion-ease-drawer); + --animate-slide-out-right: slide-out-right 160ms var(--motion-ease-out); + --animate-fade-in: fade-in 180ms var(--motion-ease-out); @keyframes accordion-down { from { @@ -247,32 +250,36 @@ 100% { transform: translateX(0); } - 25% { - transform: translateX(10px); + 18%, + 30% { + transform: translateX(6px); } - 50% { - transform: translateX(0); + 48%, + 60% { + transform: translateX(-6px); } - 75% { - transform: translateX(-10px); + 78% { + transform: translateX(0); } } } @utility animate-blink { - animation: blink 0.2s ease-in-out; + animation: blink 180ms var(--motion-ease-in-out); } /* Animation utilities for dropdown menu */ @utility animate-in { animation-name: enter; - animation-duration: 150ms; + animation-duration: var(--tw-duration, 160ms); + animation-timing-function: var(--tw-ease, var(--motion-ease-out)); animation-fill-mode: both; } @utility animate-out { animation-name: exit; - animation-duration: 150ms; + animation-duration: var(--tw-duration, 120ms); + animation-timing-function: var(--tw-ease, var(--motion-ease-out)); animation-fill-mode: both; } @@ -308,6 +315,38 @@ --enter-translate-x: 0.5rem; } +@utility slide-in-from-top { + --enter-translate-y: -100%; +} + +@utility slide-in-from-bottom { + --enter-translate-y: 100%; +} + +@utility slide-in-from-left { + --enter-translate-x: -100%; +} + +@utility slide-in-from-right { + --enter-translate-x: 100%; +} + +@utility slide-out-to-top { + --exit-translate-y: -100%; +} + +@utility slide-out-to-bottom { + --exit-translate-y: 100%; +} + +@utility slide-out-to-left { + --exit-translate-x: -100%; +} + +@utility slide-out-to-right { + --exit-translate-x: 100%; +} + @keyframes enter { from { opacity: var(--enter-opacity, 1); @@ -335,6 +374,30 @@ } } +@media (prefers-reduced-motion: reduce) { + @keyframes enter { + from { + opacity: var(--enter-opacity, 1); + transform: translate(0, 0) scale(1); + } + to { + opacity: 1; + transform: translate(0, 0) scale(1); + } + } + + @keyframes exit { + from { + opacity: 1; + transform: translate(0, 0) scale(1); + } + to { + opacity: var(--exit-opacity, 1); + transform: translate(0, 0) scale(1); + } + } +} + @utility container { margin-inline: auto; padding-inline: 2rem; diff --git a/components/action-buttons.tsx b/components/action-buttons.tsx index 895c4f818..eae5fe150 100644 --- a/components/action-buttons.tsx +++ b/components/action-buttons.tsx @@ -174,7 +174,7 @@ export function ActionButtons({ {/* Action buttons */}
@@ -204,7 +204,7 @@ export function ActionButtons({ {/* Prompt samples */}
@@ -214,8 +214,8 @@ export function ActionButtons({ key={index} type="button" className={cn( - 'w-full text-left px-3 py-2 rounded-md text-sm', - 'hover:bg-muted transition-colors', + 'w-full rounded-md px-3 py-2 text-left text-sm', + 'transition-colors duration-[140ms] ease-[var(--motion-ease-out)] hover:bg-muted', 'flex items-center gap-2 group' )} onClick={() => handlePromptClick(prompt)} diff --git a/components/artifact/artifact-context.tsx b/components/artifact/artifact-context.tsx index 113468961..a21587085 100644 --- a/components/artifact/artifact-context.tsx +++ b/components/artifact/artifact-context.tsx @@ -13,8 +13,8 @@ import type { Part } from '@/lib/types/ai' import { useSidebar } from '../ui/sidebar' -// Animation duration should match CSS transition duration -const ANIMATION_DURATION = 300 +// Animation duration should match the inspector panel exit transition. +const ANIMATION_DURATION = 260 interface ArtifactState { part: Part | null diff --git a/components/artifact/chat-artifact-container.tsx b/components/artifact/chat-artifact-container.tsx index b9ce5dfd8..dbd068ed7 100644 --- a/components/artifact/chat-artifact-container.tsx +++ b/components/artifact/chat-artifact-container.tsx @@ -132,7 +132,7 @@ export function ChatArtifactContainer({ return (
-
+
{hasUser && (!open || isMobileSidebar) && ( )} @@ -164,7 +164,8 @@ export function ChatArtifactContainer({ className={cn( 'bg-background overflow-hidden', state.isOpen && state.part ? 'opacity-100' : 'w-0 opacity-0', - !isResizing && 'transition-all duration-300 ease-out' + !isResizing && + 'transition-[opacity,width] duration-[260ms] ease-[var(--motion-ease-in-out)]' )} style={{ width: state.isOpen && state.part ? `${width}px` : '0px' diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 57f1ec3b2..436ce1e57 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -269,7 +269,7 @@ export function ChatPanel({ {messages.length > 0 && (
0 && (
- + )}