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>
)} )}
<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 /> <PendingScreenImageStrip />
{clipboardInitialText && ( {clipboardInitialText && (
<ClipboardChip <ClipboardChip
@ -900,7 +900,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser; const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser;
return ( 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"> <div className="flex items-center gap-1">
{!isDesktop ? ( {!isDesktop ? (
<> <>

View file

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

View file

@ -71,7 +71,7 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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 className
)} )}
{...props} {...props}