diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index d37ca81042..88300ca491 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -126,9 +126,14 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje )} > {content} + {/* Named + opaque so a view transition (the ask_user_question + popover ⇄ chat-card morph) paints the whole composer band + over the travelling card instead of letting it show + through below the composer. The background matches the + page, so normal rendering is unchanged. */}
diff --git a/client/src/style.css b/client/src/style.css index 91b4a47561..917a802209 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -3027,3 +3027,17 @@ html { .sharepoint-picker-bg { background-color: #f5f5f5; } + +/* ask_user_question popover <-> chat-card morph (view transition) */ +::view-transition-group(ask-question), +::view-transition-old(ask-question), +::view-transition-new(ask-question) { + animation-duration: 280ms; + animation-timing-function: cubic-bezier(0.32, 0.72, 0.22, 1); +} + +/* The composer keeps its own snapshot group stacked above the morphing + question card, so the card travels behind the ChatForm, not over it. */ +::view-transition-group(chat-form) { + z-index: 2; +} diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index 0078d1666a..58e4674984 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -11,6 +11,7 @@ export * from './share'; export * from './files'; export * from './latex'; export * from './tilde'; +export * from './morph'; export * from './forms'; export * from './roles'; export * from './errors'; diff --git a/client/src/utils/morph.ts b/client/src/utils/morph.ts new file mode 100644 index 0000000000..8ffa2b4f08 --- /dev/null +++ b/client/src/utils/morph.ts @@ -0,0 +1,21 @@ +import { flushSync } from 'react-dom'; + +/** + * Runs a state update inside a same-document view transition when supported, + * so elements sharing a `view-transition-name` morph (position, size, shape) + * between the two states instead of swapping. Falls back to applying the + * update directly. Call from user-event handlers only: the update is flushed + * synchronously so the browser can snapshot the old and new layouts. + */ +export function morphTransition(update: () => void): void { + if ( + typeof document.startViewTransition !== 'function' || + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ) { + update(); + return; + } + document.startViewTransition(() => { + flushSync(update); + }); +}