refactor: replace button elements with Button component for improved consistency and styling across multiple UI components

This commit is contained in:
Anish Sarkar 2026-05-14 14:17:44 +05:30
parent 23e05acc7c
commit 3d42712b3f
27 changed files with 401 additions and 424 deletions

View file

@ -3,6 +3,7 @@
import { Settings, Trash2, Users } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
@ -120,11 +121,13 @@ export function SearchSpaceAvatar({
);
const avatarButton = (
<button
<Button
type="button"
variant="ghost"
size="icon"
onClick={onClick}
className={cn(
"relative flex items-center justify-center rounded-lg font-semibold text-white transition-all select-none",
"relative rounded-lg font-semibold text-white transition-all select-none",
"hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
sizeClasses,
isActive && "ring-2 ring-primary ring-offset-1 ring-offset-background"
@ -144,7 +147,7 @@ export function SearchSpaceAvatar({
<Users className={cn(size === "sm" ? "h-2 w-2" : "h-2.5 w-2.5")} />
</span>
)}
</button>
</Button>
);
const menuItems = (

View file

@ -347,8 +347,9 @@ export function AllSharedChatsSidebarContent({
return (
<div key={thread.id} className="group/item relative w-full">
{isMobile ? (
<button
<Button
type="button"
variant="ghost"
onClick={() => {
if (wasLongPress()) return;
handleThreadClick(thread.id);
@ -361,7 +362,7 @@ export function AllSharedChatsSidebarContent({
onTouchMove={longPressHandlers.onTouchMove}
disabled={isBusy}
className={cn(
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground",
@ -369,16 +370,17 @@ export function AllSharedChatsSidebarContent({
)}
>
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</Button>
) : (
<Tooltip delayDuration={600}>
<TooltipTrigger asChild>
<button
<Button
type="button"
variant="ghost"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className={cn(
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground",
@ -386,7 +388,7 @@ export function AllSharedChatsSidebarContent({
)}
>
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>

View file

@ -56,19 +56,20 @@ export function ChatListItem({
return (
<div className="group/item relative w-full">
<button
<Button
type="button"
variant="ghost"
onClick={handleClick}
{...(isMobile ? longPressHandlers : {})}
className={cn(
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"h-auto w-full justify-start gap-2 overflow-hidden px-2 py-1.5 text-left font-normal",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground"
)}
>
<span className="truncate">{animatedName}</span>
</button>
</Button>
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
<div

View file

@ -575,14 +575,15 @@ export function InboxSidebarContent({
{t("filter") || "Filter"}
</p>
<div className="space-y-1">
<button
<Button
type="button"
variant="ghost"
onClick={() => {
setActiveFilter("all");
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
activeFilter === "all"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
@ -593,15 +594,16 @@ export function InboxSidebarContent({
<span>{t("all") || "All"}</span>
</span>
{activeFilter === "all" && <Check className="h-4 w-4" />}
</button>
<button
</Button>
<Button
type="button"
variant="ghost"
onClick={() => {
setActiveFilter("unread");
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
activeFilter === "unread"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
@ -612,16 +614,17 @@ export function InboxSidebarContent({
<span>{t("unread") || "Unread"}</span>
</span>
{activeFilter === "unread" && <Check className="h-4 w-4" />}
</button>
</Button>
{activeTab === "status" && (
<button
<Button
type="button"
variant="ghost"
onClick={() => {
setActiveFilter("errors");
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
activeFilter === "errors"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
@ -632,7 +635,7 @@ export function InboxSidebarContent({
<span>{t("errors_only") || "Errors only"}</span>
</span>
{activeFilter === "errors" && <Check className="h-4 w-4" />}
</button>
</Button>
)}
</div>
</div>
@ -642,14 +645,15 @@ export function InboxSidebarContent({
{t("sources") || "Sources"}
</p>
<div className="space-y-1">
<button
<Button
type="button"
variant="ghost"
onClick={() => {
setSelectedSource(null);
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
selectedSource === null
? "bg-primary/10 text-primary"
: "hover:bg-muted"
@ -660,17 +664,18 @@ export function InboxSidebarContent({
<span>{t("all_sources") || "All sources"}</span>
</span>
{selectedSource === null && <Check className="h-4 w-4" />}
</button>
</Button>
{statusSourceOptions.map((source) => (
<button
<Button
key={source.key}
type="button"
variant="ghost"
onClick={() => {
setSelectedSource(source.key);
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
selectedSource === source.key
? "bg-primary/10 text-primary"
: "hover:bg-muted"
@ -681,7 +686,7 @@ export function InboxSidebarContent({
<span>{source.displayName}</span>
</span>
{selectedSource === source.key && <Check className="h-4 w-4" />}
</button>
</Button>
))}
</div>
</div>
@ -922,11 +927,12 @@ export function InboxSidebarContent({
{activeTab === "status" ? (
<Tooltip delayDuration={600}>
<TooltipTrigger asChild>
<button
<Button
type="button"
variant="ghost"
onClick={() => handleItemClick(item)}
disabled={isMarkingAsRead}
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
className="h-auto flex-1 justify-start gap-3 overflow-hidden bg-transparent p-0 text-left hover:bg-transparent"
>
<div className="shrink-0">{getStatusIcon(item)}</div>
<div className="flex-1 min-w-0 overflow-hidden">
@ -942,7 +948,7 @@ export function InboxSidebarContent({
{convertRenderedToDisplay(item.message)}
</p>
</div>
</button>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-[250px]">
<p className="font-medium">{item.title}</p>
@ -952,11 +958,12 @@ export function InboxSidebarContent({
</TooltipContent>
</Tooltip>
) : (
<button
<Button
type="button"
variant="ghost"
onClick={() => handleItemClick(item)}
disabled={isMarkingAsRead}
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
className="h-auto flex-1 justify-start gap-3 overflow-hidden bg-transparent p-0 text-left hover:bg-transparent"
>
<div className="shrink-0">{getStatusIcon(item)}</div>
<div className="flex-1 min-w-0 overflow-hidden">
@ -972,7 +979,7 @@ export function InboxSidebarContent({
{convertRenderedToDisplay(item.message)}
</p>
</div>
</button>
</Button>
)}
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">

View file

@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { useIsAnonymous } from "@/contexts/anonymous-mode";
@ -204,13 +205,14 @@ export function Sidebar({
alwaysShowAction={!disableTooltips && isSharedChatsPanelOpen}
action={
onViewAllSharedChats ? (
<button
<Button
type="button"
variant="ghost"
onClick={onViewAllSharedChats}
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
>
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
</button>
</Button>
) : undefined
}
>
@ -260,13 +262,14 @@ export function Sidebar({
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen}
action={
onViewAllPrivateChats ? (
<button
<Button
type="button"
variant="ghost"
onClick={onViewAllPrivateChats}
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
>
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
</button>
</Button>
) : undefined
}
>

View file

@ -12,18 +12,12 @@ interface SidebarButtonProps {
isCollapsed?: boolean;
isActive?: boolean;
badge?: React.ReactNode;
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
collapsedOverlay?: React.ReactNode;
/** Custom icon node for collapsed mode — overrides the default <Icon> rendering */
collapsedIconNode?: React.ReactNode;
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
expandedIconNode?: React.ReactNode;
/** Optional inline trailing content shown in expanded mode */
trailingContent?: React.ReactNode;
/** Optional tooltip content that replaces the default label tooltip */
tooltipContent?: React.ReactNode;
className?: string;
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
}

View file

@ -29,6 +29,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { useLocaleContext } from "@/contexts/LocaleContext";
import { usePlatform } from "@/hooks/use-platform";
@ -204,11 +205,12 @@ export function SidebarUserProfile({
<div className="border-t px-1.5 py-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
<Button
type="button"
variant="ghost"
className={cn(
"mx-auto flex h-9 w-9 items-center justify-center rounded-full",
"transition-opacity hover:opacity-90",
"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"
)}
@ -220,7 +222,7 @@ export function SidebarUserProfile({
size="md"
/>
<span className="sr-only">{displayName}</span>
</button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
@ -367,10 +369,11 @@ export function SidebarUserProfile({
<div className="border-t">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
<Button
type="button"
variant="ghost"
className={cn(
"flex w-full items-center gap-2 px-2 py-3 text-left",
"h-auto w-full justify-start gap-2 rounded-none px-2 py-3 text-left",
"hover:bg-accent transition-colors",
"focus:outline-none focus-visible:outline-none",
"data-[state=open]:bg-transparent"
@ -386,7 +389,7 @@ export function SidebarUserProfile({
{/* Chevron icon */}
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>

View file

@ -10,6 +10,7 @@ import {
type Tab,
tabsAtom,
} from "@/atoms/tabs/tabs.atom";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface TabBarProps {
@ -189,22 +190,25 @@ export function TabBar({
)}
/>
) : null}
<button
type="button"
<div
data-tab-id={tab.id}
onClick={() => handleTabClick(tab)}
onMouseEnter={() => setHoveredTabIndex(index)}
onMouseLeave={() => setHoveredTabIndex(null)}
className={cn(
"group relative flex h-full items-center px-3 w-[180px] min-h-0 overflow-hidden text-[13px] font-medium rounded-md transition-colors duration-150 shrink-0",
isActive
? "bg-accent text-accent-foreground"
: "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
className="group relative h-full w-[180px] shrink-0"
>
<span className="block min-w-0 flex-1 whitespace-nowrap overflow-hidden text-left">
{tab.title}
</span>
<Button
type="button"
variant="ghost"
onClick={() => handleTabClick(tab)}
onMouseEnter={() => setHoveredTabIndex(index)}
onMouseLeave={() => setHoveredTabIndex(null)}
className={cn(
"h-full w-full justify-start overflow-hidden px-3 text-left text-[13px] font-medium transition-colors duration-150",
isActive
? "bg-accent text-accent-foreground"
: "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground group-hover:bg-accent group-hover:text-accent-foreground group-focus-within:bg-accent group-focus-within:text-accent-foreground"
)}
>
<span className="block min-w-0 flex-1 truncate text-left">{tab.title}</span>
</Button>
{/* Hover-only gradient + close overlay (sidebar pattern) — keeps pill width fixed and avoids ellipsis shift. */}
<div
className={cn(
@ -213,23 +217,19 @@ export function TabBar({
"bg-gradient-to-l from-accent from-60% to-transparent"
)}
>
{/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */}
<span
role="button"
tabIndex={0}
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => handleTabClose(e, tab.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleTabClose(e as unknown as React.MouseEvent, tab.id);
}
}}
className="pointer-events-auto rounded-full p-0.5 transition-colors hover:bg-accent hover:text-accent-foreground"
onMouseEnter={() => setHoveredTabIndex(index)}
onMouseLeave={() => setHoveredTabIndex(null)}
className="pointer-events-auto size-4 rounded-full p-0.5 hover:bg-accent hover:text-accent-foreground"
>
<X className="size-3" />
</span>
<X data-icon="inline-start" />
</Button>
</div>
</button>
</div>
</Fragment>
);
})}
@ -243,14 +243,16 @@ export function TabBar({
"before:bg-gradient-to-r before:from-transparent before:to-panel"
)}
>
<button
<Button
type="button"
variant="ghost"
size="icon"
onClick={onNewChat}
className="flex h-8 w-8 items-center justify-center shrink-0 rounded-md text-muted-foreground transition-all duration-150 hover:bg-accent hover:text-accent-foreground"
className="size-8 shrink-0 text-muted-foreground transition-all duration-150 hover:bg-accent hover:text-accent-foreground"
title="New Chat"
>
<Plus className="size-4" />
</button>
<Plus data-icon="inline-start" />
</Button>
</div>
)}
</div>