mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-03 20:32:39 +02:00
feat(sidebar): implement auto-collapse functionality and refine sidebar toggle logic
Fixed sidebar animations and icon positioning remove mobile sidebar
This commit is contained in:
parent
1b81a42ed3
commit
79a21c715e
3 changed files with 42 additions and 56 deletions
|
|
@ -112,6 +112,8 @@ function createWindow() {
|
|||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 600,
|
||||
minHeight: 480,
|
||||
show: false, // Don't show until ready
|
||||
backgroundColor: "#252525", // Prevent white flash (matches dark mode)
|
||||
titleBarStyle: "hiddenInset",
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ function formatRunTime(ts: string): string {
|
|||
}
|
||||
|
||||
function SyncStatusBar() {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const { state } = useSidebar()
|
||||
const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
|
||||
const [popoverOpen, setPopoverOpen] = useState(false)
|
||||
const [logEvents, setLogEvents] = useState<ServiceEventType[]>([])
|
||||
|
|
@ -301,7 +301,7 @@ function SyncStatusBar() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{!isMobile && isCollapsed && isSyncing && (
|
||||
{isCollapsed && isSyncing && (
|
||||
<div
|
||||
className="fixed bottom-4 z-40 flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background shadow-sm"
|
||||
style={{ left: "0.5rem" }}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,10 @@ import { Slot } from "@radix-ui/react-slot"
|
|||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-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,
|
||||
|
|
@ -28,17 +20,14 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|||
const SIDEBAR_WIDTH = 256 // 16rem in pixels
|
||||
const SIDEBAR_WIDTH_MIN = 200
|
||||
const SIDEBAR_WIDTH_MAX = 480
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars
|
||||
const SIDEBAR_AUTO_COLLAPSE_WIDTH = 760 // Auto-collapse when window narrower than this
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
sidebarWidth: number
|
||||
setSidebarWidth: (width: number) => void
|
||||
|
|
@ -70,8 +59,6 @@ function SidebarProvider({
|
|||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const [sidebarWidth, setSidebarWidth] = React.useState(SIDEBAR_WIDTH)
|
||||
const [isResizing, setIsResizing] = React.useState(false)
|
||||
|
||||
|
|
@ -94,10 +81,35 @@ function SidebarProvider({
|
|||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Auto-collapse sidebar when window is too narrow, re-expand when wide enough
|
||||
const autoCollapsedRef = React.useRef(false)
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${SIDEBAR_AUTO_COLLAPSE_WIDTH - 1}px)`)
|
||||
const onChange = () => {
|
||||
if (mql.matches && open) {
|
||||
// Window became narrow — auto-collapse
|
||||
autoCollapsedRef.current = true
|
||||
setOpen(false)
|
||||
} else if (!mql.matches && !open && autoCollapsedRef.current) {
|
||||
// Window became wide again — restore if we auto-collapsed it
|
||||
autoCollapsedRef.current = false
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
// Check on mount
|
||||
if (mql.matches && open) {
|
||||
autoCollapsedRef.current = true
|
||||
setOpen(false)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [open, setOpen])
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
autoCollapsedRef.current = false // User manually toggled, don't auto-restore
|
||||
return setOpen((open) => !open)
|
||||
}, [setOpen])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
|
|
@ -108,16 +120,13 @@ function SidebarProvider({
|
|||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
sidebarWidth,
|
||||
setSidebarWidth,
|
||||
isResizing,
|
||||
setIsResizing,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarWidth, isResizing]
|
||||
[state, open, setOpen, toggleSidebar, sidebarWidth, isResizing]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
@ -161,7 +170,7 @@ function Sidebar({
|
|||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { state } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
|
|
@ -178,34 +187,9 @@ function Sidebar({
|
|||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
className="group peer text-sidebar-foreground block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
|
|
@ -217,7 +201,7 @@ function Sidebar({
|
|||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent",
|
||||
"[[data-resizing=false]_&]:transition-[width] [[data-resizing=false]_&]:duration-200 [[data-resizing=false]_&]:ease-linear",
|
||||
"in-data-[resizing=false]:transition-[width] in-data-[resizing=false]:duration-200 in-data-[resizing=false]:ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
|
|
@ -228,10 +212,10 @@ function Sidebar({
|
|||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex",
|
||||
"[[data-resizing=false]_&]:transition-[left,right,width] [[data-resizing=false]_&]:duration-200 [[data-resizing=false]_&]:ease-linear",
|
||||
"fixed inset-y-0 z-10 flex h-svh w-(--sidebar-width)",
|
||||
"in-data-[resizing=false]:transition-[left,right,width] in-data-[resizing=false]:duration-200 in-data-[resizing=false]:ease-linear",
|
||||
side === "left"
|
||||
? "left-[var(--sidebar-offset)] group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-offset)-var(--sidebar-width))]"
|
||||
? "left-(--sidebar-offset) group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-offset)-var(--sidebar-width))]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
|
|
@ -347,7 +331,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
"peer-data-[variant=inset]:m-2 peer-data-[variant=inset]:ml-0 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:shadow-sm peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -546,7 +530,7 @@ function SidebarMenuButton({
|
|||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const { state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
|
|
@ -575,7 +559,7 @@ function SidebarMenuButton({
|
|||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
hidden={state !== "collapsed"}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue