mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-11 16:52:38 +02:00
feat: update UI components to utilize new main panel styling
- Replaced background styles with `bg-main-panel` in various components for consistent theming. - Enhanced the `Header`, `LayoutShell`, and `Thread` components to improve visual coherence. - Adjusted tool management UI to reflect new design standards, ensuring a unified user experience.
This commit is contained in:
parent
b5328a267f
commit
993c8539e8
12 changed files with 187 additions and 112 deletions
|
|
@ -9,7 +9,7 @@ export const ThreadScrollToBottom: FC = () => {
|
|||
<TooltipIconButton
|
||||
tooltip="Scroll to bottom"
|
||||
variant="outline"
|
||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent"
|
||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</TooltipIconButton>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
Globe,
|
||||
Plus,
|
||||
RefreshCwIcon,
|
||||
Settings2,
|
||||
|
|
@ -27,13 +28,13 @@ import {
|
|||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
agentToolsAtom,
|
||||
disabledToolsAtom,
|
||||
enabledToolCountAtom,
|
||||
hydrateDisabledToolsAtom,
|
||||
toggleToolAtom,
|
||||
} from "@/atoms/agent-tools/agent-tools.atoms";
|
||||
|
|
@ -118,7 +119,7 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
|
|||
const ThreadContent: FC = () => {
|
||||
return (
|
||||
<ThreadPrimitive.Root
|
||||
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background"
|
||||
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-main-panel"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "44rem",
|
||||
}}
|
||||
|
|
@ -140,7 +141,7 @@ const ThreadContent: FC = () => {
|
|||
/>
|
||||
|
||||
<ThreadPrimitive.ViewportFooter
|
||||
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6"
|
||||
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
|
||||
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
||||
>
|
||||
<ThreadScrollToBottom />
|
||||
|
|
@ -161,7 +162,7 @@ const ThreadScrollToBottom: FC = () => {
|
|||
<TooltipIconButton
|
||||
tooltip="Scroll to bottom"
|
||||
variant="outline"
|
||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent"
|
||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</TooltipIconButton>
|
||||
|
|
@ -232,7 +233,7 @@ const ThreadWelcome: FC = () => {
|
|||
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
||||
{/* Greeting positioned above the composer */}
|
||||
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
||||
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl">{greeting}</h1>
|
||||
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl select-none">{greeting}</h1>
|
||||
</div>
|
||||
{/* Composer - top edge fixed, expands downward only */}
|
||||
<div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
|
||||
|
|
@ -589,7 +590,17 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
const disabledTools = useAtomValue(disabledToolsAtom);
|
||||
const toggleTool = useSetAtom(toggleToolAtom);
|
||||
const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom);
|
||||
const enabledCount = useAtomValue(enabledToolCountAtom);
|
||||
|
||||
const hasWebSearchTool = agentTools?.some((t) => t.name === "web_search") ?? false;
|
||||
const isWebSearchEnabled = hasWebSearchTool && !disabledTools.includes("web_search");
|
||||
const filteredTools = useMemo(
|
||||
() => agentTools?.filter((t) => t.name !== "web_search"),
|
||||
[agentTools]
|
||||
);
|
||||
const filteredEnabledCount = useMemo(() => {
|
||||
if (!filteredTools) return 0;
|
||||
return filteredTools.length - disabledTools.filter((d) => filteredTools.some((t) => t.name === d)).length;
|
||||
}, [filteredTools, disabledTools]);
|
||||
|
||||
useEffect(() => {
|
||||
hydrateDisabled();
|
||||
|
|
@ -642,11 +653,11 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
<div className="flex items-center justify-between px-4 py-2">
|
||||
<DrawerTitle className="text-sm font-medium">Agent Tools</DrawerTitle>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{enabledCount}/{agentTools?.length ?? 0} enabled
|
||||
{filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
|
||||
{agentTools?.map((tool) => {
|
||||
{filteredTools?.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
return (
|
||||
<div
|
||||
|
|
@ -664,7 +675,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{!agentTools?.length && (
|
||||
{!filteredTools?.length && (
|
||||
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
Loading tools...
|
||||
</div>
|
||||
|
|
@ -707,7 +718,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">
|
||||
<span className="text-xs sm:text-sm font-medium">Agent Tools</span>
|
||||
<span className="text-[10px] sm:text-xs text-muted-foreground">
|
||||
{enabledCount}/{agentTools?.length ?? 0} enabled
|
||||
{filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -718,7 +729,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
}}
|
||||
>
|
||||
{agentTools?.map((tool) => {
|
||||
{filteredTools?.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
const row = (
|
||||
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
|
||||
|
|
@ -741,7 +752,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{!agentTools?.length && (
|
||||
{!filteredTools?.length && (
|
||||
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||
Loading tools...
|
||||
</div>
|
||||
|
|
@ -750,6 +761,39 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{hasWebSearchTool && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTool("web_search")}
|
||||
className={cn(
|
||||
"rounded-full transition-all flex items-center gap-1 px-2 py-1 border h-8",
|
||||
isWebSearchEnabled
|
||||
? "bg-sky-500/15 border-sky-500/60 text-sky-500"
|
||||
: "bg-transparent border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: isWebSearchEnabled ? 360 : 0, scale: isWebSearchEnabled ? 1.1 : 1 }}
|
||||
whileHover={{ rotate: isWebSearchEnabled ? 360 : 15, scale: 1.1, transition: { type: "spring", stiffness: 300, damping: 10 } }}
|
||||
transition={{ type: "spring", stiffness: 260, damping: 25 }}
|
||||
>
|
||||
<Globe className="size-4" />
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{isWebSearchEnabled && (
|
||||
<motion.span
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: "auto", opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-xs overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
Search
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
)}
|
||||
{sidebarDocs.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
const showExpandButton = !isMobile && collapsed && hasRightPanelContent;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4">
|
||||
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-main-panel/95 backdrop-blur supports-backdrop-filter:bg-main-panel/60 px-4">
|
||||
{/* Left side - Mobile menu trigger + Model selector */}
|
||||
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||
{mobileMenuTrigger}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
|||
return (
|
||||
<aside
|
||||
style={{ width: targetWidth }}
|
||||
className="flex h-full shrink-0 flex-col border-l bg-background overflow-hidden transition-[width] duration-200 ease-out"
|
||||
className="flex h-full shrink-0 flex-col rounded-xl border bg-background overflow-hidden transition-[width] duration-200 ease-out"
|
||||
>
|
||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export function LayoutShell({
|
|||
return (
|
||||
<SidebarProvider value={sidebarContextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div className={cn("flex h-screen w-full flex-col bg-background", className)}>
|
||||
<div className={cn("flex h-screen w-full flex-col bg-main-panel", className)}>
|
||||
<Header
|
||||
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
|
||||
/>
|
||||
|
|
@ -256,6 +256,12 @@ export function LayoutShell({
|
|||
);
|
||||
}
|
||||
|
||||
const anySlideOutOpen =
|
||||
inbox?.isOpen ||
|
||||
announcementsPanel?.open ||
|
||||
allSharedChatsPanel?.open ||
|
||||
allPrivateChatsPanel?.open;
|
||||
|
||||
// Desktop layout
|
||||
return (
|
||||
<SidebarProvider value={sidebarContextValue}>
|
||||
|
|
@ -274,8 +280,15 @@ export function LayoutShell({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Main container with sidebar and content - relative for inbox positioning */}
|
||||
<div className="relative flex flex-1 rounded-xl border bg-background overflow-hidden">
|
||||
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200",
|
||||
anySlideOutOpen
|
||||
? "rounded-l-xl border-r-0 delay-0"
|
||||
: "rounded-xl delay-150"
|
||||
)}
|
||||
>
|
||||
<Sidebar
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
|
|
@ -300,32 +313,16 @@ export function LayoutShell({
|
|||
pageUsage={pageUsage}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
className="hidden md:flex border-r shrink-0"
|
||||
className={cn(
|
||||
"flex shrink-0 transition-[border-radius] duration-200",
|
||||
anySlideOutOpen ? "rounded-l-xl delay-0" : "rounded-xl delay-150"
|
||||
)}
|
||||
isLoadingChats={isLoadingChats}
|
||||
sidebarWidth={sidebarWidth}
|
||||
onResizeMouseDown={onResizeMouseDown}
|
||||
isResizing={isResizing}
|
||||
/>
|
||||
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<Header />
|
||||
|
||||
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
||||
{documentsPanel && (
|
||||
<RightPanel
|
||||
documentsPanel={{
|
||||
open: documentsPanel.open,
|
||||
onOpenChange: documentsPanel.onOpenChange,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Inbox Sidebar - slide-out panel */}
|
||||
{/* Slide-out panels render as siblings next to the sidebar */}
|
||||
{inbox && (
|
||||
<InboxSidebar
|
||||
open={inbox.isOpen}
|
||||
|
|
@ -336,7 +333,6 @@ export function LayoutShell({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Announcements Sidebar */}
|
||||
{announcementsPanel && (
|
||||
<AnnouncementsSidebar
|
||||
open={announcementsPanel.open}
|
||||
|
|
@ -344,7 +340,6 @@ export function LayoutShell({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* All Shared Chats - slide-out panel */}
|
||||
{allSharedChatsPanel && (
|
||||
<AllSharedChatsSidebar
|
||||
open={allSharedChatsPanel.open}
|
||||
|
|
@ -353,7 +348,6 @@ export function LayoutShell({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* All Private Chats - slide-out panel */}
|
||||
{allPrivateChatsPanel && (
|
||||
<AllPrivateChatsSidebar
|
||||
open={allPrivateChatsPanel.open}
|
||||
|
|
@ -362,6 +356,37 @@ export function LayoutShell({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Resize handle — negative margins eat the flex gap so spacing stays unchanged */}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
role="slider"
|
||||
aria-label="Resize sidebar"
|
||||
tabIndex={0}
|
||||
onMouseDown={onResizeMouseDown}
|
||||
className="hidden md:block h-full cursor-col-resize z-30"
|
||||
style={{ width: 8, marginLeft: -8, marginRight: -8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content panel */}
|
||||
<div className="relative flex flex-1 flex-col rounded-xl border bg-main-panel overflow-hidden min-w-0">
|
||||
<Header />
|
||||
|
||||
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
||||
{documentsPanel && (
|
||||
<RightPanel
|
||||
documentsPanel={{
|
||||
open: documentsPanel.open,
|
||||
onOpenChange: documentsPanel.onOpenChange,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarProvider>
|
||||
|
|
|
|||
|
|
@ -856,9 +856,9 @@ export function InboxSidebar({
|
|||
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
||||
<TabsTrigger
|
||||
value="comments"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
>
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span>{t("comments") || "Comments"}</span>
|
||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||
|
|
@ -868,9 +868,9 @@ export function InboxSidebar({
|
|||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="status"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
>
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
|
||||
<History className="h-4 w-4" />
|
||||
<span>{t("status") || "Status"}</span>
|
||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ export function Sidebar({
|
|||
isLoadingChats = false,
|
||||
disableTooltips = false,
|
||||
sidebarWidth = SIDEBAR_MIN_WIDTH,
|
||||
onResizeMouseDown,
|
||||
isResizing = false,
|
||||
}: SidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
|
@ -102,19 +101,6 @@ export function Sidebar({
|
|||
)}
|
||||
style={!isCollapsed ? { width: sidebarWidth } : undefined}
|
||||
>
|
||||
{/* Resize handle on right border */}
|
||||
{!isCollapsed && onResizeMouseDown && (
|
||||
<div
|
||||
role="slider"
|
||||
aria-label="Resize sidebar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={50}
|
||||
tabIndex={0}
|
||||
onMouseDown={onResizeMouseDown}
|
||||
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
|
||||
/>
|
||||
)}
|
||||
{/* Header - search space name or collapse button when collapsed */}
|
||||
{isCollapsed ? (
|
||||
<div className="flex h-14 shrink-0 items-center justify-center border-b">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSidebarContextSafe } from "../../hooks";
|
||||
|
||||
export const SLIDEOUT_PANEL_OPENED_EVENT = "slideout-panel-opened";
|
||||
|
||||
const SIDEBAR_COLLAPSED_WIDTH = 60;
|
||||
|
||||
interface SidebarSlideOutPanelProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -19,11 +15,12 @@ interface SidebarSlideOutPanelProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reusable slide-out panel that appears from the right edge of the sidebar.
|
||||
* Used by InboxSidebar (floating mode), AllSharedChatsSidebar, and AllPrivateChatsSidebar.
|
||||
* Reusable slide-out panel that extends from the sidebar.
|
||||
*
|
||||
* Must be rendered inside a positioned container (the LayoutShell's relative flex container)
|
||||
* and within the SidebarProvider context.
|
||||
* Desktop: absolutely positioned at the sidebar's right edge, overlaying the main
|
||||
* content with a blur backdrop. Does not push/shrink the main content.
|
||||
*
|
||||
* Mobile: full-width absolute overlay (unchanged).
|
||||
*/
|
||||
export function SidebarSlideOutPanel({
|
||||
open,
|
||||
|
|
@ -33,11 +30,6 @@ export function SidebarSlideOutPanel({
|
|||
children,
|
||||
}: SidebarSlideOutPanelProps) {
|
||||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||
const sidebarContext = useSidebarContextSafe();
|
||||
const isCollapsed = sidebarContext?.isCollapsed ?? false;
|
||||
const sidebarWidth = isCollapsed
|
||||
? SIDEBAR_COLLAPSED_WIDTH
|
||||
: (sidebarContext?.sidebarWidth ?? 240);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -45,41 +37,30 @@ export function SidebarSlideOutPanel({
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
{/* Backdrop overlay with blur — desktop only, covers main content area (right of sidebar) */}
|
||||
{!isMobile && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
style={{ left: sidebarWidth }}
|
||||
className="absolute inset-y-0 right-0 z-20 bg-black/30 backdrop-blur-sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
const handleEscape = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onOpenChange(false);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
{/* Clip container - positioned at sidebar edge with overflow hidden */}
|
||||
<div
|
||||
style={{
|
||||
left: isMobile ? 0 : sidebarWidth,
|
||||
width: isMobile ? "100%" : width,
|
||||
}}
|
||||
className={cn("absolute z-30 overflow-hidden pointer-events-none", "inset-y-0")}
|
||||
>
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, handleEscape]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<div className="absolute left-0 inset-y-0 z-30 w-full overflow-hidden pointer-events-none">
|
||||
<motion.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className={cn(
|
||||
"h-full w-full bg-sidebar text-sidebar-foreground flex flex-col pointer-events-auto select-none",
|
||||
"sm:border-r sm:shadow-xl"
|
||||
)}
|
||||
className="h-full w-full bg-sidebar text-sidebar-foreground flex flex-col pointer-events-auto select-none"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel}
|
||||
|
|
@ -87,6 +68,45 @@ export function SidebarSlideOutPanel({
|
|||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{open && (
|
||||
<>
|
||||
{/* Blur backdrop covering the main content area (right of sidebar) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute z-10 bg-black/30 backdrop-blur-sm rounded-xl"
|
||||
style={{ top: -9, bottom: -9, left: "calc(100% + 1px)", width: "200vw" }}
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Panel extending from sidebar's right edge, flush with the wrapper border */}
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width }}
|
||||
exit={{ width: 0 }}
|
||||
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className="absolute z-20 overflow-hidden"
|
||||
style={{ left: "100%", top: -1, bottom: -1 }}
|
||||
>
|
||||
<div
|
||||
style={{ width }}
|
||||
className="h-full bg-sidebar text-sidebar-foreground flex flex-col select-none border rounded-r-xl shadow-xl"
|
||||
role="dialog"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -233,11 +233,11 @@ export function ModelSelector({
|
|||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("h-8 gap-2 px-3 text-sm border-border/60 select-none", className)}
|
||||
className={cn("h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none", className)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
|
@ -282,10 +282,7 @@ export function ModelSelector({
|
|||
</>
|
||||
)}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0 transition-transform duration-200",
|
||||
open && "rotate-180"
|
||||
)}
|
||||
className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue