feat(FloatingChat): refactor chat width handling to be dynamic; enhance message panel positioning and styling with glass surface effects

This commit is contained in:
Roberto Musso
2026-02-28 23:30:47 +01:00
parent f767bb5175
commit cdf9a8bf18
4 changed files with 122 additions and 32 deletions

View File

@@ -6,7 +6,7 @@ import { X, ArrowUp } from 'lucide-react';
import {
useFloatingChat,
computeDualAnchor,
CHAT_WIDTH,
getChatWidth,
CHAT_HEIGHT,
PADDING,
} from '@/context/FloatingChatContext';
@@ -66,7 +66,7 @@ function FloatingChatInner() {
if (route === 'project' && state.projectId) {
// Navigate to the project page (stay on same project)
// Project sections re-register on mount and pendingSection will auto-open
void navigate({ to: '/projects/$projectId', params: { projectId: state.projectId } });
void navigate({ to: '/projects', search: { projectId: state.projectId } });
} else if (route.startsWith('/')) {
void navigate({ to: route });
}
@@ -154,7 +154,7 @@ function FloatingChatInner() {
if (el) {
const rect = el.getBoundingClientRect();
if (rect.right > window.innerWidth || rect.bottom > window.innerHeight) {
el.style.left = `${Math.max(PADDING, Math.min(state.position.x, window.innerWidth - CHAT_WIDTH - PADDING))}px`;
el.style.left = `${Math.max(PADDING, Math.min(state.position.x, window.innerWidth - getChatWidth() - PADDING))}px`;
el.style.top = `${Math.max(PADDING, Math.min(state.position.y, window.innerHeight - CHAT_HEIGHT - PADDING))}px`;
}
}
@@ -241,6 +241,10 @@ function FloatingChatInner() {
const hasMessages = messages.length > 0 || isStreaming;
// Expand the messages panel upward if there's enough space above the input bar,
// otherwise expand downward. 320px = 300px max-h + 8px gap + 12px buffer.
const expandUp = state.position.y >= 320;
return (
<AnimatePresence>
{state.isOpen && (
@@ -260,30 +264,37 @@ function FloatingChatInner() {
width: state.position.width,
zIndex: 9999,
}}
className="flex flex-col gap-2"
className="relative"
>
{/* ---- Messages panel (appears when chat has content) ---- */}
{/* ---- Messages panel — floats above or below the input bar ---- */}
<AnimatePresence>
{hasMessages && (
<motion.div
key="messages-panel"
initial={{ opacity: 0, height: 0, scale: 0.97 }}
animate={{ opacity: 1, height: 'auto', scale: 1 }}
exit={{ opacity: 0, height: 0, scale: 0.97 }}
initial={{ opacity: 0, scale: 0.97, y: expandUp ? 8 : -8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.97, y: expandUp ? 8 : -8 }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
className="rounded-2xl"
style={{
position: 'absolute',
width: '100%',
...(expandUp
? { bottom: 'calc(100% + 8px)' }
: { top: 'calc(100% + 8px)' }),
}}
className="rounded-2xl overflow-hidden"
>
<div
ref={scrollRef}
className="max-h-[300px] overflow-y-auto rounded-2xl [&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border"
className="max-h-[300px] overflow-y-auto rounded-2xl [&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/40"
>
<div className="flex flex-col gap-2.5 p-3">
{messages.map((msg) => {
if (msg.role === 'user') {
return (
<div key={msg.id} className="flex justify-end">
<div className="max-w-[80%] rounded-2xl rounded-br-md bg-accent text-primary-foreground px-3.5 py-2 shadow-sm">
<p className="text-xs whitespace-pre-wrap leading-relaxed">
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-br-md px-3.5 py-2">
<p className="text-xs whitespace-pre-wrap leading-relaxed text-foreground">
{msg.content}
</p>
</div>
@@ -294,7 +305,7 @@ function FloatingChatInner() {
if (msg.error) {
return (
<div key={msg.id} className="flex justify-start">
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-destructive/10 px-3.5 py-2">
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2 !border-destructive/30">
<p className="text-xs text-destructive whitespace-pre-wrap leading-relaxed">
{msg.content}
</p>
@@ -305,8 +316,8 @@ function FloatingChatInner() {
return (
<div key={msg.id} className="flex justify-start">
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-primary text-primary-foreground px-3.5 py-2">
<div className="text-xs">
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2">
<div className="text-xs text-foreground">
<ChatMarkdown content={msg.content} />
</div>
</div>
@@ -317,9 +328,9 @@ function FloatingChatInner() {
{/* Streaming */}
{isStreaming && (
<div className="flex justify-start">
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-primary text-primary-foreground px-3.5 py-2">
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2">
{streamingContent ? (
<div className="text-xs">
<div className="text-xs text-foreground">
<ChatMarkdown content={streamingContent} />
</div>
) : (
@@ -338,7 +349,7 @@ function FloatingChatInner() {
</AnimatePresence>
{/* ---- Floating input bar ---- */}
<div className="relative rounded-2xl bg-background/80 backdrop-blur-2xl shadow-[0_8px_60px_-12px_rgba(0,0,0,0.5)] border border-border/30 ring-1 ring-white/5 transition-shadow focus-within:shadow-[0_8px_60px_-8px_rgba(0,0,0,0.7)] focus-within:ring-ring/20">
<div className="glass-surface relative rounded-2xl transition-shadow focus-within:shadow-[0_8px_60px_-8px_rgba(0,0,0,0.35)]">
{/* Close button */}
<button
onClick={close}