mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 11:26:24 +02:00
feat: introduce SidebarButton component for improved sidebar interactions
This commit is contained in:
parent
9a20db7fc4
commit
affc89dd5c
3 changed files with 126 additions and 74 deletions
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { CheckCircle2, CircleAlert } from "lucide-react";
|
import { CheckCircle2, CircleAlert } from "lucide-react";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { NavItem } from "../../types/layout.types";
|
import type { NavItem } from "../../types/layout.types";
|
||||||
|
import { SidebarButton } from "./SidebarButton";
|
||||||
|
|
||||||
interface NavSectionProps {
|
interface NavSectionProps {
|
||||||
items: NavItem[];
|
items: NavItem[];
|
||||||
|
|
@ -66,71 +66,48 @@ function StatusIcon({
|
||||||
return <FallbackIcon className={cn("shrink-0", className)} />;
|
return <FallbackIcon className={cn("shrink-0", className)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CollapsedOverlay({ item }: { item: NavItem }) {
|
||||||
|
const indicator = item.statusIndicator;
|
||||||
|
if (indicator && indicator !== "idle") {
|
||||||
|
return <StatusBadge status={indicator} />;
|
||||||
|
}
|
||||||
|
if (item.badge) {
|
||||||
|
return (
|
||||||
|
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function NavSection({ items, onItemClick, isCollapsed = false }: NavSectionProps) {
|
export function NavSection({ items, onItemClick, isCollapsed = false }: NavSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const Icon = item.icon;
|
|
||||||
const indicator = item.statusIndicator;
|
|
||||||
|
|
||||||
const joyrideAttr =
|
const joyrideAttr =
|
||||||
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
|
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
|
||||||
? { "data-joyride": "inbox-sidebar" }
|
? { "data-joyride": "inbox-sidebar" as const }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
if (isCollapsed) {
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={item.url}>
|
<SidebarButton
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onItemClick?.(item)}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
||||||
)}
|
|
||||||
{...joyrideAttr}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{indicator && indicator !== "idle" ? (
|
|
||||||
<StatusBadge status={indicator} />
|
|
||||||
) : item.badge ? (
|
|
||||||
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
|
|
||||||
{item.badge}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="sr-only">{item.title}</span>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
{item.title}
|
|
||||||
{item.badge && ` (${item.badge})`}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.url}
|
key={item.url}
|
||||||
type="button"
|
icon={item.icon}
|
||||||
|
label={item.title}
|
||||||
onClick={() => onItemClick?.(item)}
|
onClick={() => onItemClick?.(item)}
|
||||||
className={cn(
|
isCollapsed={isCollapsed}
|
||||||
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
|
badge={item.badge}
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
collapsedOverlay={<CollapsedOverlay item={item} />}
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
expandedIconNode={
|
||||||
)}
|
<StatusIcon
|
||||||
{...joyrideAttr}
|
status={item.statusIndicator}
|
||||||
>
|
FallbackIcon={item.icon}
|
||||||
<StatusIcon status={indicator} FallbackIcon={Icon} className="h-4 w-4" />
|
className="h-4 w-4"
|
||||||
<span className="flex-1 truncate">{item.title}</span>
|
/>
|
||||||
{item.badge && (
|
}
|
||||||
<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">
|
buttonProps={joyrideAttr}
|
||||||
{item.badge}
|
/>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@
|
||||||
import { PenSquare } from "lucide-react";
|
import { PenSquare } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
import { ChatListItem } from "./ChatListItem";
|
import { ChatListItem } from "./ChatListItem";
|
||||||
import { NavSection } from "./NavSection";
|
import { NavSection } from "./NavSection";
|
||||||
import { PageUsageDisplay } from "./PageUsageDisplay";
|
import { PageUsageDisplay } from "./PageUsageDisplay";
|
||||||
|
import { SidebarButton } from "./SidebarButton";
|
||||||
import { SidebarCollapseButton } from "./SidebarCollapseButton";
|
import { SidebarCollapseButton } from "./SidebarCollapseButton";
|
||||||
import { SidebarHeader } from "./SidebarHeader";
|
import { SidebarHeader } from "./SidebarHeader";
|
||||||
import { SidebarSection } from "./SidebarSection";
|
import { SidebarSection } from "./SidebarSection";
|
||||||
|
|
@ -132,23 +131,13 @@ export function Sidebar({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* New chat button */}
|
{/* New chat button */}
|
||||||
<div className="p-2">
|
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
||||||
{isCollapsed ? (
|
<SidebarButton
|
||||||
<Tooltip>
|
icon={PenSquare}
|
||||||
<TooltipTrigger asChild>
|
label={t("new_chat")}
|
||||||
<Button variant="outline" size="icon" className="w-full h-10" onClick={onNewChat}>
|
onClick={onNewChat}
|
||||||
<PenSquare className="h-4 w-4" />
|
isCollapsed={isCollapsed}
|
||||||
<span className="sr-only">{t("new_chat")}</span>
|
/>
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">{t("new_chat")}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Button variant="outline" className="w-full justify-start gap-2" onClick={onNewChat}>
|
|
||||||
<PenSquare className="h-4 w-4" />
|
|
||||||
{t("new_chat")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat sections - fills available space */}
|
{/* Chat sections - fills available space */}
|
||||||
|
|
|
||||||
86
surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
Normal file
86
surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type React from "react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface SidebarButtonProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
isCollapsed?: 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 expanded mode — overrides the default <Icon> rendering */
|
||||||
|
expandedIconNode?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
|
||||||
|
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedClassName = cn(
|
||||||
|
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
);
|
||||||
|
|
||||||
|
const collapsedClassName = cn(
|
||||||
|
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
);
|
||||||
|
|
||||||
|
export function SidebarButton({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
isCollapsed = false,
|
||||||
|
badge,
|
||||||
|
collapsedOverlay,
|
||||||
|
expandedIconNode,
|
||||||
|
className,
|
||||||
|
buttonProps,
|
||||||
|
}: SidebarButtonProps) {
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(collapsedClassName, className)}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{collapsedOverlay}
|
||||||
|
<span className="sr-only">{label}</span>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
{label}
|
||||||
|
{typeof badge === "string" && ` (${badge})`}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(expandedClassName, className)}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{expandedIconNode ?? <Icon className="h-4 w-4 shrink-0" />}
|
||||||
|
<span className="flex-1 truncate">{label}</span>
|
||||||
|
{badge && typeof badge !== "string" ? badge : null}
|
||||||
|
{badge && typeof badge === "string" ? (
|
||||||
|
<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">
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue