refactor: update SidebarUserProfile and Composer components with improved styling and tooltip integration

This commit is contained in:
Anish Sarkar 2026-05-14 23:22:32 +05:30
parent 4083d33b5c
commit 2bdd59611a
3 changed files with 187 additions and 170 deletions

View file

@ -735,7 +735,7 @@ const Composer: FC = () => {
/>
</div>
)}
<div className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow">
<div className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-3xl border-input bg-muted pt-2 shadow-sm shadow-black/5 outline-none transition-shadow dark:shadow-black/10">
<PendingScreenImageStrip />
{clipboardInitialText && (
<ClipboardChip
@ -900,7 +900,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser;
return (
<div className="aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between">
<div className="aui-composer-action-wrapper relative mx-3 mb-3 flex items-center justify-between">
<div className="flex items-center gap-1">
{!isDesktop ? (
<>

View file

@ -31,7 +31,9 @@ import {
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useLocaleContext } from "@/contexts/LocaleContext";
import { useMediaQuery } from "@/hooks/use-media-query";
import { usePlatform } from "@/hooks/use-platform";
import { GITHUB_RELEASES_URL, usePrimaryDownload } from "@/lib/desktop-download-utils";
import { APP_VERSION } from "@/lib/env-config";
@ -133,15 +135,15 @@ function UserAvatar({
bgColor: string;
size?: "sm" | "md";
}) {
const sizeClass = size === "md" ? "h-9 w-9" : "h-8 w-8";
const sizeClass = size === "md" ? "h-10 w-10" : "h-8 w-8";
if (avatarUrl) {
return (
<Image
src={avatarUrl}
alt="User avatar"
width={size === "md" ? 36 : 32}
height={size === "md" ? 36 : 32}
width={size === "md" ? 40 : 32}
height={size === "md" ? 40 : 32}
className={cn(sizeClass, "shrink-0 rounded-full object-cover select-none")}
referrerPolicy="no-referrer"
unoptimized
@ -175,6 +177,7 @@ export function SidebarUserProfile({
const t = useTranslations("sidebar");
const { locale, setLocale } = useLocaleContext();
const { isDesktop } = usePlatform();
const isDesktopViewport = useMediaQuery("(min-width: 768px)");
const { os, primary } = usePrimaryDownload();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const bgColor = stringToColor(user.email);
@ -182,6 +185,7 @@ export function SidebarUserProfile({
const displayName = user.name || user.email.split("@")[0];
const downloadUrl = primary?.url ?? GITHUB_RELEASES_URL;
const downloadLabel = t("download_for_os", { os });
const showDownloadCta = !isDesktop && isDesktopViewport;
const handleLanguageChange = (newLocale: "en" | "es" | "pt" | "hi" | "zh") => {
setLocale(newLocale);
@ -204,100 +208,140 @@ export function SidebarUserProfile({
// Collapsed view - just show avatar with dropdown
if (isCollapsed) {
return (
<div className="border-t px-1.5 py-2">
<Button
asChild
variant="ghost"
size="icon"
className="mx-auto mb-2 h-9 w-9 rounded-md bg-muted hover:bg-accent"
>
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={downloadLabel}
onClick={() =>
trackDesktopDownloadClicked({ os, placement: "sidebar_collapsed" })
}
>
<Download className="h-4 w-4" strokeWidth={2.5} />
</a>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
className={cn(
"mx-auto h-9 w-9 rounded-full p-0",
"transition-opacity hover:bg-transparent hover:opacity-90",
"focus:outline-none focus-visible:outline-none",
"data-[state=open]:opacity-90"
)}
>
<UserAvatar
avatarUrl={user.avatarUrl}
initials={initials}
bgColor={bgColor}
size="md"
/>
<span className="sr-only">{displayName}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-medium">{displayName}</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<UserCog className="h-4 w-4" />
{t("user_settings")}
</DropdownMenuItem>
{onAnnouncements && (
<DropdownMenuItem onClick={onAnnouncements}>
<Megaphone className="h-4 w-4" />
<span className="flex-1">What's New</span>
{announcementUnreadCount > 0 && (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{formatAnnouncementCount(announcementUnreadCount)}
</span>
<div className="w-full border-t px-1.5 py-2">
<div className="flex flex-col items-center gap-2">
{showDownloadCta && (
<Tooltip>
<TooltipTrigger asChild>
<Button
asChild
variant="ghost"
size="icon"
className="h-10 w-10 rounded-lg bg-muted hover:bg-accent"
>
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={downloadLabel}
onClick={() =>
trackDesktopDownloadClicked({ os, placement: "sidebar_collapsed" })
}
>
<Download className="h-4 w-4" strokeWidth={2.5} />
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{downloadLabel}
</TooltipContent>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
className={cn(
"h-10 w-10 rounded-full p-0",
"transition-opacity hover:bg-transparent hover:opacity-90",
"focus:outline-none focus-visible:outline-none",
"data-[state=open]:opacity-90"
)}
</DropdownMenuItem>
)}
>
<UserAvatar
avatarUrl={user.avatarUrl}
initials={initials}
bgColor={bgColor}
size="md"
/>
<span className="sr-only">{displayName}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-medium">{displayName}</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<UserCog className="h-4 w-4" />
{t("user_settings")}
</DropdownMenuItem>
{onAnnouncements && (
<DropdownMenuItem onClick={onAnnouncements}>
<Megaphone className="h-4 w-4" />
<span className="flex-1">What's New</span>
{announcementUnreadCount > 0 && (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{formatAnnouncementCount(announcementUnreadCount)}
</span>
)}
</DropdownMenuItem>
)}
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="h-4 w-4" />
{t("theme")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="gap-1">
{THEMES.map((themeOption) => {
const Icon = themeOption.icon;
const isSelected = theme === themeOption.value;
return (
<DropdownMenuItem
key={themeOption.value}
onClick={() => handleThemeChange(themeOption.value)}
className={cn(
"mb-1 last:mb-0 transition-all",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "text-primary"
)}
>
<Icon className="h-4 w-4" />
<span className="flex-1">{t(themeOption.value)}</span>
{isSelected && <Check className="h-4 w-4 shrink-0" />}
</DropdownMenuItem>
);
})}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="h-4 w-4" />
{t("theme")}
<Languages className="h-4 w-4" />
{t("language")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="gap-1">
{THEMES.map((themeOption) => {
const Icon = themeOption.icon;
const isSelected = theme === themeOption.value;
{LANGUAGES.map((language) => {
const isSelected = locale === language.code;
return (
<DropdownMenuItem
key={themeOption.value}
onClick={() => handleThemeChange(themeOption.value)}
key={language.code}
onClick={() => handleLanguageChange(language.code)}
className={cn(
"mb-1 last:mb-0 transition-all",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "text-primary"
)}
>
<Icon className="h-4 w-4" />
<span className="flex-1">{t(themeOption.value)}</span>
<span className="mr-2">{language.flag}</span>
<span className="flex-1">{language.name}</span>
{isSelected && <Check className="h-4 w-4 shrink-0" />}
</DropdownMenuItem>
);
@ -305,81 +349,52 @@ export function SidebarUserProfile({
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="h-4 w-4" />
{t("language")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="gap-1">
{LANGUAGES.map((language) => {
const isSelected = locale === language.code;
return (
<DropdownMenuItem
key={language.code}
onClick={() => handleLanguageChange(language.code)}
className={cn(
"mb-1 last:mb-0 transition-all",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "text-primary"
)}
>
<span className="mr-2">{language.flag}</span>
<span className="flex-1">{language.name}</span>
{isSelected && <Check className="h-4 w-4 shrink-0" />}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Info className="h-4 w-4" />
{t("learn_more")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="min-w-[180px] gap-1">
{LEARN_MORE_LINKS.map((link) => (
<DropdownMenuItem key={link.key} asChild>
<a href={link.href} target="_blank" rel="noopener noreferrer">
<span className="flex-1">{t(link.key)}</span>
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</a>
</DropdownMenuItem>
);
})}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
))}
<DropdownMenuSeparator />
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
v{APP_VERSION}
</p>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Info className="h-4 w-4" />
{t("learn_more")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="min-w-[180px] gap-1">
{LEARN_MORE_LINKS.map((link) => (
<DropdownMenuItem key={link.key} asChild>
<a href={link.href} target="_blank" rel="noopener noreferrer">
<span className="flex-1">{t(link.key)}</span>
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</a>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
v{APP_VERSION}
</p>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{!isDesktop && (
<DropdownMenuItem asChild className="font-medium">
<a href={downloadUrl} target="_blank" rel="noopener noreferrer">
<Download className="h-4 w-4" strokeWidth={2.5} />
{downloadLabel}
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
{isLoggingOut ? (
<Spinner size="sm" className="mr-2" />
) : (
<LogOut className="h-4 w-4" />
{!isDesktop && (
<DropdownMenuItem asChild className="font-medium">
<a href={downloadUrl} target="_blank" rel="noopener noreferrer">
<Download className="h-4 w-4" strokeWidth={2.5} />
{downloadLabel}
</a>
</DropdownMenuItem>
)}
{isLoggingOut ? t("loggingOut") : t("logout")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
{isLoggingOut ? (
<Spinner size="sm" className="mr-2" />
) : (
<LogOut className="h-4 w-4" />
)}
{isLoggingOut ? t("loggingOut") : t("logout")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
@ -387,21 +402,23 @@ export function SidebarUserProfile({
// Expanded view
return (
<div className="border-t">
<Button
asChild
variant="ghost"
className="mx-2 mt-2 mb-1 h-10 w-[calc(100%-1rem)] justify-start gap-2 rounded-md bg-muted px-3 text-sm font-semibold hover:bg-accent"
>
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackDesktopDownloadClicked({ os, placement: "sidebar_expanded" })}
{showDownloadCta && (
<Button
asChild
variant="ghost"
className="mx-2 mt-2 mb-1 h-10 w-[calc(100%-1rem)] justify-start gap-2 rounded-md bg-muted px-3 text-sm font-semibold hover:bg-accent"
>
<Download className="h-4 w-4" strokeWidth={2.5} />
{downloadLabel}
</a>
</Button>
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackDesktopDownloadClicked({ os, placement: "sidebar_expanded" })}
>
<Download className="h-4 w-4" strokeWidth={2.5} />
{downloadLabel}
</a>
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button

View file

@ -71,7 +71,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
"bg-neutral-800 text-white font-medium shadow-xl px-3 py-1.5 dark:bg-neutral-800 dark:text-white border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
className
)}
{...props}