feat: Added tooltips for navbar buttons to clear meaning of each one (#1636)

* feat: Added tooltips for navbar buttons to clear meaning of each one.

* update tooltips and fix linter errors

---------

Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
Fahleen Arif
2026-01-28 23:39:15 +05:00
committed by GitHub
parent 42da1e5a52
commit 425c8d2bdf
6 changed files with 156 additions and 125 deletions

View File

@@ -1,21 +1,29 @@
import { useLingui } from "@lingui/react/macro" import { Trans, useLingui } from "@lingui/react/macro"
import { LanguagesIcon } from "lucide-react" import { LanguagesIcon } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { dynamicActivate } from "@/lib/i18n" import { dynamicActivate } from "@/lib/i18n"
import languages from "@/lib/languages" import languages from "@/lib/languages"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
export function LangToggle() { export function LangToggle() {
const { i18n } = useLingui() const { i18n } = useLingui()
const LangTrans = <Trans>Language</Trans>
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger>
<Button variant={"ghost"} size="icon" className="hidden sm:flex"> <Tooltip>
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" /> <TooltipTrigger asChild>
<span className="sr-only">Language</span> <Button variant={"ghost"} size="icon" className="hidden sm:flex">
</Button> <LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
<span className="sr-only">{LangTrans}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{LangTrans}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="grid grid-cols-3"> <DropdownMenuContent className="grid grid-cols-3">
{languages.map(([lang, label, e]) => ( {languages.map(([lang, label, e]) => (

View File

@@ -2,19 +2,28 @@ import { t } from "@lingui/core/macro"
import { MoonStarIcon, SunIcon } from "lucide-react" import { MoonStarIcon, SunIcon } from "lucide-react"
import { useTheme } from "@/components/theme-provider" import { useTheme } from "@/components/theme-provider"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
import { Trans } from "@lingui/react/macro"
export function ModeToggle() { export function ModeToggle() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
return ( return (
<Button <Tooltip>
variant={"ghost"} <TooltipTrigger>
size="icon" <Button
aria-label={t`Toggle theme`} variant={"ghost"}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")} size="icon"
> aria-label={t`Toggle theme`}
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" /> onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" /> >
</Button> <SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" />
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" />
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans>Toggle theme</Trans>
</TooltipContent>
</Tooltip>
) )
} }

View File

@@ -30,7 +30,7 @@ import { LangToggle } from "./lang-toggle"
import { Logo } from "./logo" import { Logo } from "./logo"
import { ModeToggle } from "./mode-toggle" import { ModeToggle } from "./mode-toggle"
import { $router, basePath, Link, prependBasePath } from "./router" import { $router, basePath, Link, prependBasePath } from "./router"
import { t } from "@lingui/core/macro" import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
const CommandPalette = lazy(() => import("./command-palette")) const CommandPalette = lazy(() => import("./command-palette"))
@@ -49,30 +49,52 @@ export default function Navbar() {
</Link> </Link>
<SearchButton /> <SearchButton />
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}> <div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
<Link <Tooltip>
href={getPagePath($router, "containers")} <TooltipTrigger asChild>
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))} <Link
aria-label="Containers" href={getPagePath($router, "containers")}
> className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
<ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} /> aria-label="Containers"
</Link> >
<Link <ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
href={getPagePath($router, "smart")} </Link>
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))} </TooltipTrigger>
aria-label="S.M.A.R.T." <TooltipContent>
> <Trans>All Containers</Trans>
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} /> </TooltipContent>
</Link> </Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={getPagePath($router, "smart")}
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
aria-label="S.M.A.R.T."
>
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
</Link>
</TooltipTrigger>
<TooltipContent>
<Trans>S.M.A.R.T.</Trans>
</TooltipContent>
</Tooltip>
<LangToggle /> <LangToggle />
<ModeToggle /> <ModeToggle />
<Link <Tooltip>
href={getPagePath($router, "settings", { name: "general" })} <TooltipTrigger asChild>
aria-label="Settings" <Link
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))} href={getPagePath($router, "settings", { name: "general" })}
> aria-label="Settings"
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" /> className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
</Link> >
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
</Link>
</TooltipTrigger>
<TooltipContent>
<Trans>Settings</Trans>
</TooltipContent>
</Tooltip>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}> <button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
@@ -135,15 +157,15 @@ export default function Navbar() {
) )
} }
const Kbd = ({ children }: { children: React.ReactNode }) => (
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{children}
</kbd>
)
function SearchButton() { function SearchButton() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const Kbd = ({ children }: { children: React.ReactNode }) => (
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{children}
</kbd>
)
return ( return (
<> <>
<Button <Button

View File

@@ -17,7 +17,7 @@ import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons" import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums" import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils" import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types" import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
@@ -135,43 +135,41 @@ export default function InfoBar({
<div> <div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1> <h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90"> <div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <div className="capitalize flex gap-2 items-center">
<div className="capitalize flex gap-2 items-center"> <span className={cn("relative flex h-3 w-3")}>
<span className={cn("relative flex h-3 w-3")}> {system.status === SystemStatus.Up && (
{system.status === SystemStatus.Up && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
)}
<span <span
className={cn("relative inline-flex rounded-full h-3 w-3", { className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
"bg-green-500": system.status === SystemStatus.Up, style={{ animationDuration: "1.5s" }}
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span> ></span>
</span> )}
{translatedStatus} <span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</TooltipTrigger>
{system.info.ct && (
<TooltipContent>
<div className="flex gap-1 items-center">
{system.info.ct === ConnectionType.WebSocket ? (
<WebSocketIcon className="size-4" />
) : (
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
)}
{connectionTypeLabels[system.info.ct as ConnectionType]}
</div> </div>
</TooltipTrigger> </TooltipContent>
{system.info.ct && ( )}
<TooltipContent> </Tooltip>
<div className="flex gap-1 items-center">
{system.info.ct === ConnectionType.WebSocket ? (
<WebSocketIcon className="size-4" />
) : (
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
)}
{connectionTypeLabels[system.info.ct as ConnectionType]}
</div>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{systemInfo.map(({ value, label, Icon, hide }) => { {systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) { if (hide || !value) {
@@ -186,12 +184,10 @@ export default function InfoBar({
<div key={value} className="contents"> <div key={value} className="contents">
<Separator orientation="vertical" className="h-4 bg-primary/30" /> <Separator orientation="vertical" className="h-4 bg-primary/30" />
{label ? ( {label ? (
<TooltipProvider> <Tooltip delayDuration={100}>
<Tooltip delayDuration={150}> <TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipTrigger asChild>{content}</TooltipTrigger> <TooltipContent>{label}</TooltipContent>
<TooltipContent>{label}</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
) : ( ) : (
content content
)} )}
@@ -202,26 +198,24 @@ export default function InfoBar({
</div> </div>
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1"> <div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} /> <ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
<TooltipProvider delayDuration={100}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button aria-label={t`Toggle grid`}
aria-label={t`Toggle grid`} variant="outline"
variant="outline" size="icon"
size="icon" className="hidden xl:flex p-0 text-primary"
className="hidden xl:flex p-0 text-primary" onClick={() => setGrid(!grid)}
onClick={() => setGrid(!grid)} >
> {grid ? (
{grid ? ( <LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" /> ) : (
) : ( <Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" /> )}
)} </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>{t`Toggle grid`}</TooltipContent>
<TooltipContent>{t`Toggle grid`}</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -3,7 +3,7 @@ import { CopyIcon } from "lucide-react"
import { copyToClipboard } from "@/lib/utils" import { copyToClipboard } from "@/lib/utils"
import { Button } from "./button" import { Button } from "./button"
import { Input } from "./input" import { Input } from "./input"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) { export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {
return ( return (
@@ -14,25 +14,23 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none" "h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
} }
></div> ></div>
<TooltipProvider delayDuration={100} disableHoverableContent> <Tooltip disableHoverableContent={true}>
<Tooltip disableHoverableContent={true}> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button type="button"
type="button" variant={"link"}
variant={"link"} className="absolute end-0 top-0"
className="absolute end-0 top-0" onClick={() => copyToClipboard(value)}
onClick={() => copyToClipboard(value)} >
> <CopyIcon className="size-4" />
<CopyIcon className="size-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> <p>
<p> <Trans>Click to copy</Trans>
<Trans>Click to copy</Trans> </p>
</p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
</div> </div>
) )
} }

View File

@@ -3,7 +3,7 @@ import type * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { function TooltipProvider({ delayDuration = 50, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
} }