diff --git a/components/ai_chat/resources/page/components/conversations_list/index.tsx b/components/ai_chat/resources/page/components/conversations_list/index.tsx
index 96bece4d647..4fbe61beceb 100644
--- a/components/ai_chat/resources/page/components/conversations_list/index.tsx
+++ b/components/ai_chat/resources/page/components/conversations_list/index.tsx
@@ -115,32 +115,35 @@ function ConversationItem(props: ConversationItemProps) {
{title}
-
-
-
-
-
-
{getLocale(S.CHAT_UI_MENU_RENAME_CONVERSATION)}
-
-
-
-
-
-
{getLocale(S.CHAT_UI_MENU_DELETE_CONVERSATION)}
-
-
-
+
+
+
+
+
{getLocale(S.CHAT_UI_MENU_RENAME_CONVERSATION)}
+
+
+
+
+
+
{getLocale(S.CHAT_UI_MENU_DELETE_CONVERSATION)}
+
+
+
+
{uuid === aiChatContext.editingConversationId && (
diff --git a/components/ai_chat/resources/page/components/full_page/index.tsx b/components/ai_chat/resources/page/components/full_page/index.tsx
index 2dd95b65b78..c4b056ea30d 100644
--- a/components/ai_chat/resources/page/components/full_page/index.tsx
+++ b/components/ai_chat/resources/page/components/full_page/index.tsx
@@ -20,110 +20,54 @@ export default function FullScreen() {
const { createNewConversation } = useActiveChat()
const conversationContext = useConversation()
- const asideAnimationRef = React.useRef
()
- const controllerRef = React.useRef(new AbortController())
+ const asideRef = React.useRef(null)
const isSmall = useIsSmall()
- const [isNavigationCollapsed, setIsNavigationCollapsed] =
- React.useState(isSmall)
- const [isNavigationRendered, setIsNavigationRendered] =
- React.useState(!isSmall)
+
+ // Enable CSS transitions after the first paint to avoid animating on mount
+ const [isAnimated, setIsAnimated] = React.useState(false)
+ React.useEffect(() => {
+ const frame = requestAnimationFrame(() => setIsAnimated(true))
+ return () => cancelAnimationFrame(frame)
+ }, [])
const canStartNewConversation =
aiChatContext.hasAcceptedAgreement
&& !!conversationContext.conversationHistory.length
- const asideRef = React.useRef(null)
-
- const initAsideAnimation = React.useCallback((node: HTMLElement | null) => {
- asideRef.current = node
-
- if (!node) return
- const open = { width: 'var(--navigation-width)', opacity: 1 }
- const close = { width: '0px', opacity: 0 }
- const animationOptions: KeyframeAnimationOptions = {
- duration: 400,
- easing: 'cubic-bezier(0.77, 0, 0.175, 1)',
- fill: 'forwards',
- }
- asideAnimationRef.current = new Animation(
- new KeyframeEffect(node, [open, close], animationOptions),
- )
-
- // Make sure we're in the right state for our screen size when
- asideAnimationRef.current.playbackRate = isSmall ? 1 : -1
- asideAnimationRef.current.finish()
- }, [])
-
- const toggleAside = () => {
- const asideAnimation = asideAnimationRef.current
-
- if (asideAnimation) {
- if (isNavigationCollapsed) {
- controllerRef.current.abort()
- controllerRef.current = new AbortController()
- asideAnimation.ready.then(() => setIsNavigationRendered(true))
- asideAnimation.playbackRate = -1
- } else {
- // 'finish' triggers in both directions, so we only need this once per close animation
- // user may rapidly toggle the aside, so we need to abort scheduled listener in open animation
- asideAnimation.addEventListener(
- 'finish',
- () => setIsNavigationRendered(false),
- { once: true, signal: controllerRef.current.signal },
- )
- asideAnimation.playbackRate = 1
- }
-
- asideAnimation.play()
- setIsNavigationCollapsed(!isNavigationCollapsed)
- }
- }
-
+ // When editing a conversation title, ensure the sidebar is open
React.useEffect(() => {
- const isOpen = asideAnimationRef.current?.playbackRate === 1
- if (aiChatContext.editingConversationId && isOpen) {
- toggleAside()
+ if (aiChatContext.editingConversationId && !aiChatContext.showSidebar) {
+ aiChatContext.toggleSidebar()
}
- }, [aiChatContext.editingConversationId, isNavigationCollapsed])
+ }, [aiChatContext.editingConversationId])
// On iOS, close the sidebar when the delete-conversation dialog opens.
// The sidebar overlaps the dialog and intercepts touch events, requiring
- // a double-tap to reach buttons inside the dialog. Uses toggleSidebar()
- // (not toggleAside()) so showSidebar stays in sync with the visual state;
- // the showSidebar effect above then drives the actual animation.
+ // a double-tap to reach buttons inside a dialog. Uses toggleSidebar()
+ // so showSidebar stays in sync with the visual state.
//
React.useEffect(() => {
- const isClosed = asideAnimationRef.current?.playbackRate === 1
- if (aiChatContext.deletingConversationId && !isClosed) {
+ if (aiChatContext.deletingConversationId && aiChatContext.showSidebar) {
aiChatContext.toggleSidebar()
}
}, [aiChatContext.deletingConversationId])
//
React.useEffect(() => {
- const isOpen = asideAnimationRef.current?.playbackRate === 1
-
// We've just changed to small and the sidebar was open, so close it
- if (isSmall && !isOpen) {
+ if (isSmall && aiChatContext.showSidebar) {
aiChatContext.toggleSidebar()
}
// We've just changed to big and the sidebar was closed, so open it
- if (!isSmall && isOpen) {
+ if (!isSmall && !aiChatContext.showSidebar) {
aiChatContext.toggleSidebar()
}
}, [isSmall])
- React.useEffect(() => {
- const isOpen = asideAnimationRef.current?.playbackRate === 1
- if (isOpen !== aiChatContext.showSidebar) {
- toggleAside()
- }
- }, [aiChatContext.showSidebar])
-
// Add handler for closing the sidebar when clicking outside of it.
React.useEffect(() => {
- if (aiChatContext.showSidebar || !isSmall) return
+ if (!aiChatContext.showSidebar || !isSmall) return
const handleClick = (e: MouseEvent) => {
const path = e.composedPath()
// On iOS, the one-tap fix dispatches synthetic clicks at (0,0).
@@ -173,7 +117,7 @@ export default function FullScreen() {
>
- {!isNavigationRendered && canStartNewConversation && (
+ {!aiChatContext.showSidebar && canStartNewConversation && (
<>
diff --git a/components/ai_chat/resources/page/components/full_page/style.module.scss b/components/ai_chat/resources/page/components/full_page/style.module.scss
index cc2d0698661..2e2d06b2c62 100644
--- a/components/ai_chat/resources/page/components/full_page/style.module.scss
+++ b/components/ai_chat/resources/page/components/full_page/style.module.scss
@@ -42,10 +42,27 @@
.aside {
--navigation-width: 340px;
- width: var(--navigation-width);
+ --animation-duration: 200ms;
+ width: 0;
+ opacity: 0;
+ visibility: hidden;
height: 100%;
overflow: hidden;
background: var(--leo-color-page-background);
+ padding-top: var(--top-padding);
+
+ &.open {
+ width: var(--navigation-width);
+ opacity: 1;
+ visibility: visible;
+ }
+
+ &.animated {
+ transition:
+ width var(--animation-duration) cubic-bezier(0.77, 0, 0.175, 1),
+ opacity var(--animation-duration) cubic-bezier(0.77, 0, 0.175, 1),
+ visibility var(--animation-duration) allow-discrete;
+ }
@media (max-width: 1024px) {
position: fixed;
@@ -56,8 +73,6 @@
0px var(--leo-elevation-xs, 4px) var(--leo-elevation-xs, 16px) -2px
var(--leo-color-elevation-secondary, rgba(0, 0, 0, 0.1));
}
-
- padding-top: var(--top-padding);
}
.nav {
diff --git a/components/ai_chat/resources/page/state/ai_chat_context.tsx b/components/ai_chat/resources/page/state/ai_chat_context.tsx
index 9f2022368af..25e9c672495 100644
--- a/components/ai_chat/resources/page/state/ai_chat_context.tsx
+++ b/components/ai_chat/resources/page/state/ai_chat_context.tsx
@@ -39,7 +39,7 @@ export default function useProvideAIChatContext(props: AIChatContextProps) {
string | null
>(null)
const isSmall = useIsSmall()
- const [showSidebar, setShowSidebar] = React.useState(isSmall)
+ const [showSidebar, setShowSidebar] = React.useState(!isSmall)
const [skillDialog, setSkillDialog] = React.useState(null)
const { getConversationsData, isPlaceholderData: isConversationsLoading } =