mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-28 10:02:36 +02:00
Compare commits
1 Commits
agent/lamb
...
codex/desk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130c75d1ec |
@@ -49,6 +49,7 @@
|
||||
"electron-updater": "^6.8.3",
|
||||
"fix-path": "^5.0.0",
|
||||
"lucide-react": "catalog:",
|
||||
"motion": "^12.38.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-router-dom": "^7.6.0",
|
||||
|
||||
@@ -144,7 +144,7 @@ function createWindow(): void {
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
titleBarStyle: "hiddenInset",
|
||||
trafficLightPosition: { x: 16, y: 13 },
|
||||
trafficLightPosition: { x: 16, y: 17 },
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
// Windows/Linux pick up the window/taskbar icon from this option.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useSyncExternalStore } from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { useTabHistory } from "@/hooks/use-tab-history";
|
||||
import { useActiveTitleSync } from "@/hooks/use-tab-sync";
|
||||
@@ -22,41 +23,69 @@ import { TabBar } from "./tab-bar";
|
||||
import { TabContent } from "./tab-content";
|
||||
import { WindowOverlay } from "./window-overlay";
|
||||
|
||||
function SidebarTopBar() {
|
||||
const TOP_BAR_HEIGHT_CLASS = "h-12";
|
||||
const WINDOW_TOOLBAR_CLEARANCE = 184;
|
||||
const toolbarMotion = {
|
||||
type: "spring",
|
||||
stiffness: 420,
|
||||
damping: 38,
|
||||
mass: 0.8,
|
||||
} as const;
|
||||
|
||||
function WindowToolbar() {
|
||||
const { canGoBack, canGoForward, goBack, goForward } = useTabHistory();
|
||||
const navButtonClassName =
|
||||
"flex size-7 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-30";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-12 shrink-0 flex items-center justify-end px-2"
|
||||
className={cn(
|
||||
"fixed left-0 top-0 z-30 flex w-[184px] shrink-0 items-center px-3",
|
||||
TOP_BAR_HEIGHT_CLASS,
|
||||
)}
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-0.5"
|
||||
className="flex items-center gap-1 pl-[70px]"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
disabled={!canGoBack}
|
||||
aria-label="Go back"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-30 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goForward}
|
||||
disabled={!canGoForward}
|
||||
aria-label="Go forward"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-30 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
<SidebarTrigger
|
||||
className="size-7 text-muted-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
disabled={!canGoBack}
|
||||
aria-label="Go back"
|
||||
title="Go back"
|
||||
className={navButtonClassName}
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goForward}
|
||||
disabled={!canGoForward}
|
||||
aria-label="Go forward"
|
||||
title="Go forward"
|
||||
className={navButtonClassName}
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTopSpacer() {
|
||||
return <div className={cn("shrink-0", TOP_BAR_HEIGHT_CLASS)} />;
|
||||
}
|
||||
|
||||
function useNativeNavigationGestures() {
|
||||
const { goBack, goForward } = useTabHistory();
|
||||
|
||||
@@ -72,30 +101,31 @@ function useNativeNavigationGestures() {
|
||||
}
|
||||
|
||||
// The main area's top bar doubles as a window drag region. When the sidebar
|
||||
// is not occupying main-flow width — either user-collapsed (offcanvas) or
|
||||
// auto-hidden in mobile mode (<768px, becomes a sheet drawer) — we pad the
|
||||
// left side so tabs don't land under the macOS traffic lights (which live at
|
||||
// roughly x=16..68 and always hit-test above HTML), and surface a trigger so
|
||||
// the sidebar can be brought back without keyboard shortcut.
|
||||
// is not occupying main-flow width, leave room for the fixed window toolbar
|
||||
// so tabs do not land beneath the traffic lights / navigation controls.
|
||||
function MainTopBar() {
|
||||
const { state, isMobile } = useSidebar();
|
||||
const sidebarHidden = state === "collapsed" || isMobile;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"h-12 shrink-0 flex items-center gap-2",
|
||||
sidebarHidden && "pl-20",
|
||||
)}
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
<motion.header
|
||||
animate={{ paddingLeft: sidebarHidden ? WINDOW_TOOLBAR_CLEARANCE : 0 }}
|
||||
className={cn("relative shrink-0 flex items-center gap-2", TOP_BAR_HEIGHT_CLASS)}
|
||||
initial={false}
|
||||
transition={toolbarMotion}
|
||||
>
|
||||
{sidebarHidden && (
|
||||
<SidebarTrigger
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
/>
|
||||
)}
|
||||
<TabBar />
|
||||
</header>
|
||||
<motion.div
|
||||
aria-hidden
|
||||
animate={{ left: sidebarHidden ? WINDOW_TOOLBAR_CLEARANCE : 0 }}
|
||||
className="absolute inset-y-0 right-0"
|
||||
initial={false}
|
||||
transition={toolbarMotion}
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
/>
|
||||
<div className="relative z-10 flex h-full items-center">
|
||||
<TabBar />
|
||||
</div>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,9 +213,10 @@ export function DesktopShell() {
|
||||
<DesktopInboxBridge />
|
||||
<div className="flex h-screen">
|
||||
<SidebarProvider className="flex-1">
|
||||
{slug && <AppSidebar topSlot={<SidebarTopBar />} searchSlot={<SearchTrigger />} />}
|
||||
{slug && <WindowToolbar />}
|
||||
{slug && <AppSidebar topSlot={<SidebarTopSpacer />} searchSlot={<SearchTrigger />} />}
|
||||
{/* Right side: header + content container */}
|
||||
<div className="flex flex-1 min-w-0 flex-col">
|
||||
<motion.div layout transition={toolbarMotion} className="flex flex-1 min-w-0 flex-col">
|
||||
<MainTopBar />
|
||||
{/* Content area with inset styling — relative so ChatWindow/ChatFab are constrained here */}
|
||||
<div className="relative flex flex-1 min-h-0 flex-col overflow-hidden mr-2 mb-2 ml-0.5 rounded-xl shadow-sm bg-background">
|
||||
@@ -193,7 +224,7 @@ export function DesktopShell() {
|
||||
{slug && <ChatWindow />}
|
||||
{slug && <ChatFab />}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
{slug && <ModalRegistry />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { motion, useReducedMotion } from "motion/react"
|
||||
|
||||
import { useIsMobile } from "@multica/ui/hooks/use-mobile"
|
||||
import { cn } from "@multica/ui/lib/utils"
|
||||
@@ -173,7 +174,12 @@ function Sidebar({
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile, isResizing } = useSidebar()
|
||||
const { isMobile, state, openMobile, setOpenMobile, isResizing, width } = useSidebar()
|
||||
const reducedMotion = useReducedMotion()
|
||||
const animateOffcanvas = collapsible === "offcanvas"
|
||||
const motionTransition = reducedMotion
|
||||
? { duration: 0 }
|
||||
: { type: "spring" as const, stiffness: 420, damping: 38, mass: 0.8 }
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
@@ -226,31 +232,49 @@ function Sidebar({
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
<motion.div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent",
|
||||
!isResizing && "transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
!animateOffcanvas && !isResizing && "transition-[width] duration-200 ease-linear",
|
||||
!animateOffcanvas && "group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
? !animateOffcanvas && "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: !animateOffcanvas && "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
animate={animateOffcanvas ? { width: state === "collapsed" ? 0 : width } : undefined}
|
||||
initial={false}
|
||||
transition={motionTransition}
|
||||
/>
|
||||
<div
|
||||
<motion.div
|
||||
data-slot="sidebar-container"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
||||
!isResizing && "transition-[left,right,width] duration-200 ease-linear",
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex",
|
||||
animateOffcanvas
|
||||
? side === "left"
|
||||
? "left-0"
|
||||
: "right-0"
|
||||
: "data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
!animateOffcanvas && !isResizing && "transition-[left,right,width] duration-200 ease-linear",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
animate={
|
||||
animateOffcanvas
|
||||
? {
|
||||
x: state === "collapsed" ? (side === "left" ? -width : width) : 0,
|
||||
width,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
initial={false}
|
||||
transition={motionTransition}
|
||||
{...(props as React.ComponentProps<typeof motion.div>)}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
@@ -259,7 +283,7 @@ function Sidebar({
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"linkify-it": "^5.0.0",
|
||||
"katex": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"motion": "^12.38.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -218,6 +218,25 @@
|
||||
color: var(--sidebar-accent-foreground);
|
||||
}
|
||||
|
||||
/* Right detail sidebars use react-resizable-panels, which sizes panels by
|
||||
* updating the outer panel's flex-grow. Animate that property for button
|
||||
* toggles, but disable it while the resize handle is actively dragged. */
|
||||
[data-right-sidebar-panel] {
|
||||
transition-property: flex-grow;
|
||||
transition-duration: 220ms;
|
||||
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
[data-group]:has(> [data-separator="active"]) > [data-right-sidebar-panel] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-right-sidebar-panel] {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sonner toast: align icon to first line of text, not vertically centered */
|
||||
[data-sonner-toast] {
|
||||
align-items: flex-start !important;
|
||||
|
||||
@@ -87,6 +87,11 @@ import { ProgressRing } from "./progress-ring";
|
||||
import { matchesPinyin } from "../../editor/extensions/pinyin-match";
|
||||
import { useT } from "../../i18n";
|
||||
import { useIssueDetailScrollRestore } from "../hooks/use-issue-detail-scroll-restore";
|
||||
import {
|
||||
AnimatedRightSidebar,
|
||||
rightSidebarPanelMotionProps,
|
||||
useAnimatedRightSidebarState,
|
||||
} from "../../layout/animated-right-sidebar";
|
||||
|
||||
function SubscriberPopoverContent({
|
||||
members,
|
||||
@@ -683,7 +688,12 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
});
|
||||
const sidebarRef = usePanelRef();
|
||||
const isMobile = useIsMobile();
|
||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(defaultSidebarOpen);
|
||||
const {
|
||||
open: desktopSidebarOpen,
|
||||
visualOpen: desktopSidebarVisualOpen,
|
||||
beginToggle: beginDesktopSidebarToggle,
|
||||
handleResize: handleDesktopSidebarResize,
|
||||
} = useAnimatedRightSidebarState(defaultSidebarOpen);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1296,9 +1306,11 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
|
||||
const panel = sidebarRef.current;
|
||||
if (!panel) return;
|
||||
if (panel.isCollapsed()) panel.expand();
|
||||
const nextOpen = panel.isCollapsed();
|
||||
beginDesktopSidebarToggle(nextOpen);
|
||||
if (nextOpen) panel.expand();
|
||||
else panel.collapse();
|
||||
}, [isMobile, sidebarRef]);
|
||||
}, [beginDesktopSidebarToggle, isMobile, sidebarRef]);
|
||||
|
||||
useIssueDetailScrollRestore({
|
||||
restoreKey: `${wsId}:${id}`,
|
||||
@@ -2155,19 +2167,18 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
id="sidebar"
|
||||
{...rightSidebarPanelMotionProps}
|
||||
defaultSize={defaultSidebarOpen ? 320 : 0}
|
||||
minSize={260}
|
||||
maxSize={420}
|
||||
collapsible
|
||||
groupResizeBehavior="preserve-pixel-size"
|
||||
panelRef={sidebarRef}
|
||||
onResize={(size) => setDesktopSidebarOpen(size.inPixels > 0)}
|
||||
onResize={handleDesktopSidebarResize}
|
||||
>
|
||||
<div className="overflow-y-auto border-l h-full">
|
||||
<div className="p-4">
|
||||
<AnimatedRightSidebar open={desktopSidebarVisualOpen}>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedRightSidebar>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
|
||||
90
packages/views/layout/animated-right-sidebar.tsx
Normal file
90
packages/views/layout/animated-right-sidebar.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
|
||||
export const rightSidebarPanelMotionProps = {
|
||||
"data-right-sidebar-panel": "true",
|
||||
style: { overflowX: "hidden" },
|
||||
} as const;
|
||||
|
||||
const RIGHT_SIDEBAR_PANEL_TRANSITION_MS = 220;
|
||||
const RIGHT_SIDEBAR_PANEL_SETTLE_MS = RIGHT_SIDEBAR_PANEL_TRANSITION_MS + 80;
|
||||
|
||||
const rightSidebarTransition = {
|
||||
type: "spring",
|
||||
stiffness: 420,
|
||||
damping: 38,
|
||||
mass: 0.8,
|
||||
} as const;
|
||||
|
||||
export function useAnimatedRightSidebarState(initialOpen: boolean) {
|
||||
const [open, setOpen] = useState(initialOpen);
|
||||
const [visualOpen, setVisualOpen] = useState(initialOpen);
|
||||
const toggleTargetRef = useRef<boolean | null>(null);
|
||||
const settleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const beginToggle = useCallback((nextOpen: boolean) => {
|
||||
toggleTargetRef.current = nextOpen;
|
||||
setOpen(nextOpen);
|
||||
setVisualOpen(nextOpen);
|
||||
|
||||
if (settleTimeoutRef.current) {
|
||||
clearTimeout(settleTimeoutRef.current);
|
||||
}
|
||||
|
||||
settleTimeoutRef.current = setTimeout(() => {
|
||||
toggleTargetRef.current = null;
|
||||
settleTimeoutRef.current = null;
|
||||
}, RIGHT_SIDEBAR_PANEL_SETTLE_MS);
|
||||
}, []);
|
||||
|
||||
const handleResize = useCallback((size: { inPixels: number }) => {
|
||||
const nextOpen = size.inPixels > 0;
|
||||
const toggleTarget = toggleTargetRef.current;
|
||||
|
||||
if (toggleTarget === null) {
|
||||
setOpen(nextOpen);
|
||||
setVisualOpen(nextOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(toggleTarget);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (settleTimeoutRef.current) {
|
||||
clearTimeout(settleTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { open, visualOpen, beginToggle, handleResize };
|
||||
}
|
||||
|
||||
export function AnimatedRightSidebar({
|
||||
open,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
open: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: open ? 1 : 0, x: open ? 0 : 12 }}
|
||||
className={cn(
|
||||
"h-full overflow-x-hidden overflow-y-auto border-l",
|
||||
!open && "pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
initial={false}
|
||||
transition={rightSidebarTransition}
|
||||
>
|
||||
<div className="p-4">{children}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -72,6 +72,11 @@ import {
|
||||
} from "@multica/ui/components/ui/tooltip";
|
||||
import { EmojiPicker } from "@multica/ui/components/common/emoji-picker";
|
||||
import { BreadcrumbHeader } from "../../layout/breadcrumb-header";
|
||||
import {
|
||||
AnimatedRightSidebar,
|
||||
rightSidebarPanelMotionProps,
|
||||
useAnimatedRightSidebarState,
|
||||
} from "../../layout/animated-right-sidebar";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -464,7 +469,12 @@ export function ProjectDetail({ projectId }: { projectId: string }) {
|
||||
// to `true` made the mobile <Sheet> mount in the open position on first render
|
||||
// (after `useIsMobile()` flipped from false→true), briefly covering the page
|
||||
// with its modal backdrop and locking scroll — leaving the page unresponsive.
|
||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true);
|
||||
const {
|
||||
open: desktopSidebarOpen,
|
||||
visualOpen: desktopSidebarVisualOpen,
|
||||
beginToggle: beginDesktopSidebarToggle,
|
||||
handleResize: handleDesktopSidebarResize,
|
||||
} = useAnimatedRightSidebarState(true);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const sidebarOpen = isMobile ? mobileSidebarOpen : desktopSidebarOpen;
|
||||
|
||||
@@ -474,6 +484,20 @@ export function ProjectDetail({ projectId }: { projectId: string }) {
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
const handleToggleSidebar = useCallback(() => {
|
||||
if (isMobile) {
|
||||
setMobileSidebarOpen((open) => !open);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = sidebarRef.current;
|
||||
if (!panel) return;
|
||||
const nextOpen = panel.isCollapsed();
|
||||
beginDesktopSidebarToggle(nextOpen);
|
||||
if (nextOpen) panel.expand();
|
||||
else panel.collapse();
|
||||
}, [beginDesktopSidebarToggle, isMobile, sidebarRef]);
|
||||
|
||||
// Lead popover
|
||||
const [leadOpen, setLeadOpen] = useState(false);
|
||||
const [leadFilter, setLeadFilter] = useState("");
|
||||
@@ -798,16 +822,7 @@ export function ProjectDetail({ projectId }: { projectId: string }) {
|
||||
variant={sidebarOpen ? "secondary" : "ghost"}
|
||||
size="icon-sm"
|
||||
className={sidebarOpen ? "" : "text-muted-foreground"}
|
||||
onClick={() => {
|
||||
if (isMobile) {
|
||||
setMobileSidebarOpen((open) => !open);
|
||||
} else {
|
||||
const panel = sidebarRef.current;
|
||||
if (!panel) return;
|
||||
if (panel.isCollapsed()) panel.expand();
|
||||
else panel.collapse();
|
||||
}
|
||||
}}
|
||||
onClick={handleToggleSidebar}
|
||||
>
|
||||
<PanelRight />
|
||||
</Button>
|
||||
@@ -832,19 +847,18 @@ export function ProjectDetail({ projectId }: { projectId: string }) {
|
||||
{!isMobile && (
|
||||
<ResizablePanel
|
||||
id="sidebar"
|
||||
{...rightSidebarPanelMotionProps}
|
||||
defaultSize={desktopSidebarOpen ? 320 : 0}
|
||||
minSize={260}
|
||||
maxSize={420}
|
||||
collapsible
|
||||
groupResizeBehavior="preserve-pixel-size"
|
||||
panelRef={sidebarRef}
|
||||
onResize={(size) => setDesktopSidebarOpen(size.inPixels > 0)}
|
||||
onResize={handleDesktopSidebarResize}
|
||||
>
|
||||
<div className="overflow-y-auto border-l h-full">
|
||||
<div className="p-4">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
<AnimatedRightSidebar open={desktopSidebarVisualOpen}>
|
||||
{sidebarContent}
|
||||
</AnimatedRightSidebar>
|
||||
</ResizablePanel>
|
||||
)}
|
||||
{isMobile && (
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -187,6 +187,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.1(react@19.2.3)
|
||||
motion:
|
||||
specifier: ^12.38.0
|
||||
version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.3
|
||||
@@ -863,6 +866,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.1(react@19.2.3)
|
||||
motion:
|
||||
specifier: ^12.38.0
|
||||
version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
|
||||
Reference in New Issue
Block a user