Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions src/renderer/src/components/browser-ui/browser-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { CollapseMode, SidebarSide } from "@/components/browser-ui/main";
import { ScrollableSidebarContent } from "@/components/browser-ui/sidebar/content/sidebar-content";
import { SidebarFooterUpdate } from "@/components/browser-ui/sidebar/footer/update";
import { NavigationControls } from "@/components/browser-ui/sidebar/header/action-buttons";
import { SidebarAddressBar } from "@/components/browser-ui/sidebar/header/address-bar/address-bar";
import { SidebarWindowControls } from "@/components/browser-ui/sidebar/header/window-controls";
import { SidebarSpacesSwitcher } from "@/components/browser-ui/sidebar/spaces-switcher";
import { PortalComponent } from "@/components/portal/portal";
import { useSpaces } from "@/components/providers/spaces-provider";
import {
Sidebar,
SidebarFooter,
Expand All @@ -6,21 +15,13 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
useSidebar
useSidebar,
type SidebarVariant
} from "@/components/ui/resizable-sidebar";
import { useEffect, useRef, useState, useCallback } from "react";
import { cn } from "@/lib/utils";
import { CollapseMode, SidebarVariant, SidebarSide } from "@/components/browser-ui/main";
import { PlusIcon, SettingsIcon } from "lucide-react";
import { SidebarSpacesSwitcher } from "@/components/browser-ui/sidebar/spaces-switcher";
import { ScrollableSidebarContent } from "@/components/browser-ui/sidebar/content/sidebar-content";
import { useSpaces } from "@/components/providers/spaces-provider";
import { NavigationControls } from "@/components/browser-ui/sidebar/header/action-buttons";
import { SidebarAddressBar } from "@/components/browser-ui/sidebar/header/address-bar/address-bar";
import { PortalComponent } from "@/components/portal/portal";
import { SidebarWindowControls } from "@/components/browser-ui/sidebar/header/window-controls";
import { motion, AnimatePresence } from "motion/react";
import { SidebarFooterUpdate } from "@/components/browser-ui/sidebar/footer/update";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";

type BrowserSidebarProps = {
collapseMode: CollapseMode;
Expand Down Expand Up @@ -56,6 +57,7 @@ function useSidebarAnimation(shouldRenderContent: boolean, setVariant: (variant:

// Custom hook to handle sidebar hover state
function useSidebarHover(setIsHoveringSidebar: (isHovering: boolean) => void) {
const { pinned } = useSidebar();
const isHoveringSidebarRef = useRef(false);
const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);

Expand All @@ -72,16 +74,18 @@ function useSidebarHover(setIsHoveringSidebar: (isHovering: boolean) => void) {
}, [setIsHoveringSidebar]);

const handleMouseLeave = useCallback(() => {
if (pinned) return;

isHoveringSidebarRef.current = false;
timeoutIdRef.current = setTimeout(() => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
if (!isHoveringSidebarRef.current) {
if (!isHoveringSidebarRef.current && !pinned) {
setIsHoveringSidebar(false);
}
}, 100);
}, [setIsHoveringSidebar]);
}, [pinned, setIsHoveringSidebar]);

return { handleMouseEnter, handleMouseLeave };
}
Expand Down
33 changes: 15 additions & 18 deletions src/renderer/src/components/browser-ui/main.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import BrowserContent from "@/components/browser-ui/browser-content";
import { SidebarInset, SidebarProvider, useSidebar } from "@/components/ui/resizable-sidebar";
import { motion, AnimatePresence } from "motion/react";
import { cn } from "@/lib/utils";
import { BrowserSidebar } from "@/components/browser-ui/browser-sidebar";
import { SpacesProvider } from "@/components/providers/spaces-provider";
import { useEffect, useMemo, useRef } from "react";
import { useState } from "react";
import { TabsProvider, useTabs } from "@/components/providers/tabs-provider";
import { SettingsProvider, useSettings } from "@/components/providers/settings-provider";
import { SidebarAddressBar } from "@/components/browser-ui/sidebar/header/address-bar/address-bar";
import { SidebarHoverDetector } from "@/components/browser-ui/sidebar/hover-detector";
import { TabDisabler } from "@/components/logic/tab-disabler";
import { ActionsProvider } from "@/components/providers/actions-provider";
import { AppUpdatesProvider } from "@/components/providers/app-updates-provider";
import { BrowserActionProvider } from "@/components/providers/browser-action-provider";
import { ExtensionsProviderWithSpaces } from "@/components/providers/extensions-provider";
import { SidebarHoverDetector } from "@/components/browser-ui/sidebar/hover-detector";
import MinimalToastProvider from "@/components/providers/minimal-toast-provider";
import { AppUpdatesProvider } from "@/components/providers/app-updates-provider";
import { ActionsProvider } from "@/components/providers/actions-provider";
import { SidebarAddressBar } from "@/components/browser-ui/sidebar/header/address-bar/address-bar";
import { SettingsProvider, useSettings } from "@/components/providers/settings-provider";
import { SpacesProvider } from "@/components/providers/spaces-provider";
import { TabsProvider, useTabs } from "@/components/providers/tabs-provider";
import { SidebarInset, SidebarProvider, useSidebar } from "@/components/ui/resizable-sidebar";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";

export type CollapseMode = "icon" | "offcanvas";
export type SidebarVariant = "sidebar" | "floating";
export type SidebarSide = "left" | "right";

export type WindowType = "main" | "popup";

function InternalBrowserUI({ isReady, type }: { isReady: boolean; type: WindowType }) {
const { open, setOpen } = useSidebar();
const { open, setOpen, pinned, variant, setVariant } = useSidebar();
const { getSetting } = useSettings();
const { focusedTab, tabGroups } = useTabs();

const [variant, setVariant] = useState<SidebarVariant>("sidebar");
const [isHoveringSidebar, setIsHoveringSidebar] = useState(false);

const side: SidebarSide = getSetting<SidebarSide>("sidebarSide") ?? "left";
Expand All @@ -54,10 +50,11 @@ function InternalBrowserUI({ isReady, type }: { isReady: boolean; type: WindowTy
const isActiveTabLoading = focusedTab?.isLoading || false;

useEffect(() => {
if (!isHoveringSidebar && open && variant === "floating") {
// Only auto-hide floating sidebar if it's not pinned
if (!isHoveringSidebar && open && variant === "floating" && !pinned) {
setOpen(false);
}
}, [isHoveringSidebar, open, variant, setOpen, setVariant]);
}, [isHoveringSidebar, open, variant, pinned, setOpen]);

// Only show the browser content if the focused tab is in full screen mode
if (focusedTab?.fullScreen) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
SidebarMenuItem,
useSidebar
} from "@/components/ui/resizable-sidebar";
import { NavigationEntry } from "~/flow/interfaces/browser/navigation";
import { cn } from "@/lib/utils";
import { SidebarCloseIcon, SidebarOpenIcon, XIcon } from "lucide-react";
import { Pin, PinOff, SidebarCloseIcon, SidebarOpenIcon, XIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { ComponentProps, useCallback, useEffect, useRef, useState } from "react";
import { NavigationEntry } from "~/flow/interfaces/browser/navigation";
import { TabData } from "~/types/tabs";

export type NavigationEntryWithIndex = NavigationEntry & { index: number };
Expand Down Expand Up @@ -94,9 +94,22 @@ function StopLoadingIcon() {
);
}

function PinButton() {
const { pinned, togglePin } = useSidebar();

return (
<SidebarActionButton
icon={pinned ? <PinOff className="size-4" /> : <Pin className="size-4" />}
onClick={togglePin}
className={SIDEBAR_HOVER_COLOR}
title={pinned ? "Unpin sidebar" : "Pin sidebar"}
/>
);
}

export function NavigationControls() {
const { focusedTab } = useTabs();
const { open, setOpen } = useSidebar();
const { open, setOpen, variant } = useSidebar();

const [entries, setEntries] = useState<NavigationEntryWithIndex[]>([]);
const [activeIndex, setActiveIndex] = useState(0);
Expand Down Expand Up @@ -164,6 +177,7 @@ export function NavigationControls() {
onClick={closeSidebar}
className={SIDEBAR_HOVER_COLOR}
/>
{variant === "floating" && <PinButton />}

{/* Browser Actions */}
<BrowserActionList alignmentY="bottom" alignmentX="right" />
Expand Down
59 changes: 52 additions & 7 deletions src/renderer/src/components/ui/resizable-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import * as React from "react";
import { Slot as SlotPrimitive } from "radix-ui";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { Slot as SlotPrimitive } from "radix-ui";
import * as React from "react";

import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/use-mobile";
import { useSidebarResize } from "@/hooks/use-sidebar-resize";
import { mergeButtonRefs } from "@/lib/merge-button-refs";
import { cn } from "@/lib/utils";

export type SidebarVariant = "sidebar" | "floating";

const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_PINNED_COOKIE_NAME = "sidebar_pinned";
const SIDEBAR_PINNED_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
Expand All @@ -27,6 +31,8 @@ const MIN_SIDEBAR_WIDTH = "14rem";
const MAX_SIDEBAR_WIDTH = "22rem";

type SidebarContextProps = {
variant: SidebarVariant;
setVariant: (variant: SidebarVariant) => void;
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
Expand All @@ -41,6 +47,11 @@ type SidebarContextProps = {

isDraggingRail: boolean;
setIsDraggingRail: (isDraggingRail: boolean) => void;

// Pinning Properties
pinned: boolean;
setPinned: (pinned: boolean) => void;
togglePin: () => void;
};

const SidebarContext = React.createContext<SidebarContextProps | null>(null);
Expand Down Expand Up @@ -74,6 +85,28 @@ function SidebarProvider({

const [width, setWidth] = React.useState(defaultWidth);
const [isDraggingRail, setIsDraggingRail] = React.useState(false);
const [variant, setVariant] = React.useState<SidebarVariant>("sidebar");

// Pinning state management
const [pinned, setPinnedState] = React.useState(() => {
// Try to get pinned state from cookie first
const cookies = document.cookie.split(";");
const pinnedCookie = cookies.find((cookie) => cookie.trim().startsWith(`${SIDEBAR_PINNED_COOKIE_NAME}=`));
if (pinnedCookie) {
return pinnedCookie.split("=")[1] === "true";
}
return false;
});

const setPinned = React.useCallback((value: boolean) => {
setPinnedState(value);
// Persist pinned state in cookie
document.cookie = `${SIDEBAR_PINNED_COOKIE_NAME}=${value}; path=/; max-age=${SIDEBAR_PINNED_COOKIE_MAX_AGE}`;
}, []);

const togglePin = React.useCallback(() => {
setPinned(prev => !prev);
}, [setPinned]);

// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
Expand Down Expand Up @@ -122,6 +155,8 @@ function SidebarProvider({

const contextValue = React.useMemo<SidebarContextProps>(
() => ({
variant,
setVariant,
state,
open,
setOpen,
Expand All @@ -134,9 +169,15 @@ function SidebarProvider({
setWidth,

isDraggingRail,
setIsDraggingRail
setIsDraggingRail,

pinned,
setPinned,
togglePin
}),
[
variant,
setVariant,
state,
open,
setOpen,
Expand All @@ -147,7 +188,10 @@ function SidebarProvider({
width,
setWidth,
isDraggingRail,
setIsDraggingRail
setIsDraggingRail,
pinned,
setPinned,
togglePin
]
);

Expand Down Expand Up @@ -185,7 +229,7 @@ function Sidebar({
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
const { isMobile, state, openMobile, setOpenMobile, pinned } = useSidebar();

if (collapsible === "none") {
return (
Expand Down Expand Up @@ -233,6 +277,7 @@ function Sidebar({
data-variant={variant}
data-side={side}
data-slot="sidebar"
data-pinned={pinned}
>
{/* This is what handles the sidebar gap on desktop */}
<div
Expand Down