Skip to content
Draft
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
24 changes: 0 additions & 24 deletions apps/app/src/actions/sidebar.ts

This file was deleted.

14 changes: 7 additions & 7 deletions apps/app/src/app/(app)/[orgId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { AnimatedLayout } from '@/components/animated-layout';
import { AppSidebar } from '@/components/app-sidebar';
import { CheckoutCompleteDialog } from '@/components/dialogs/checkout-complete-dialog';
import { Header } from '@/components/header';
import { AssistantSheet } from '@/components/sheets/assistant-sheet';
import { Sidebar } from '@/components/sidebar';
import { TriggerTokenProvider } from '@/components/trigger-token-provider';
import { SidebarProvider } from '@/context/sidebar-context';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { auth } from '@/utils/auth';
import { db } from '@db';
import dynamic from 'next/dynamic';
Expand All @@ -29,7 +28,7 @@ export default async function Layout({
const { orgId: requestedOrgId } = await params;

const cookieStore = await cookies();
const isCollapsed = cookieStore.get('sidebar-collapsed')?.value === 'true';
const defaultOpen = cookieStore.get('sidebar_state')?.value !== 'false';
let publicAccessToken = cookieStore.get('publicAccessToken')?.value || undefined;

// Check if user has access to this organization
Expand Down Expand Up @@ -89,8 +88,9 @@ export default async function Layout({
triggerJobId={onboarding?.triggerJobId || undefined}
initialToken={publicAccessToken || undefined}
>
<SidebarProvider initialIsCollapsed={isCollapsed}>
<AnimatedLayout sidebar={<Sidebar organization={organization} />} isCollapsed={isCollapsed}>
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar organization={organization} />
<SidebarInset>
{onboarding?.triggerJobId && <ConditionalOnboardingTracker onboarding={onboarding} />}
<Header organizationId={organization.id} />
<ConditionalPaddingWrapper>
Expand All @@ -100,7 +100,7 @@ export default async function Layout({
<Suspense fallback={null}>
<CheckoutCompleteDialog orgId={organization.id} />
</Suspense>
</AnimatedLayout>
</SidebarInset>
<HotKeys />
</SidebarProvider>
</TriggerTokenProvider>
Expand Down
25 changes: 0 additions & 25 deletions apps/app/src/components/animated-layout.tsx

This file was deleted.

33 changes: 33 additions & 0 deletions apps/app/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getOrganizations } from '@/data/getOrganizations';
import type { Organization } from '@db';
import { MainMenu } from './main-menu';
import { OrganizationSwitcher } from './organization-switcher';
import { SidebarCollapseButton } from './sidebar-collapse-button';
import { SidebarLogo } from './sidebar-logo';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from './ui/sidebar';

export async function AppSidebar({ organization }: { organization: Organization | null }) {
const { organizations } = await getOrganizations();

return (
<Sidebar collapsible="icon" className="bg-card overflow-x-clip">
<SidebarHeader className="p-4 gap-0">
<div className="flex items-center justify-start h-10">
<SidebarLogo />
</div>
<div className="mt-2 flex flex-col gap-2">
<OrganizationSwitcher organizations={organizations} organization={organization} />
<MainMenu organizationId={organization?.id ?? ''} organization={organization} />
</div>
</SidebarHeader>

<SidebarContent className="flex-1" />

<SidebarFooter className="p-0">
<SidebarCollapseButton />
</SidebarFooter>

<SidebarRail />
</Sidebar>
);
}
95 changes: 95 additions & 0 deletions apps/app/src/components/layout/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client';

import { cn } from '@comp/ui/cn';
import { useEffect, useState } from 'react';

const SIDEBAR_COOKIE_NAME = 'sidebar-collapsed';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year

interface AppShellProps {
children: React.ReactNode;
sidebar: React.ReactNode;
defaultCollapsed?: boolean;
}

export function AppShell({ children, sidebar, defaultCollapsed = false }: AppShellProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
const savedState = localStorage.getItem(SIDEBAR_COOKIE_NAME);
if (savedState !== null) {
setIsCollapsed(savedState === 'true');
}
}, []);

const toggleSidebar = () => {
const newState = !isCollapsed;
setIsCollapsed(newState);
localStorage.setItem(SIDEBAR_COOKIE_NAME, String(newState));
document.cookie = `${SIDEBAR_COOKIE_NAME}=${newState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
};

return (
<div
className="grid h-dvh w-full overflow-hidden md:grid-cols-[80px_1fr]"
data-sidebar-collapsed={isCollapsed}
>
{/* Sidebar Container - Fixed width rail on desktop */}
<div
className={cn(
'bg-card relative hidden overflow-hidden border-r md:block',
'transition-none', // No transition on container
// Mobile: full overlay
'md:w-[80px]',
)}
>
{/* Sidebar Panel - Slides in/out */}
<aside
className={cn(
'bg-card absolute inset-0 w-[240px] overflow-y-auto overflow-x-hidden border-r',
'transform-gpu transition-transform duration-200 ease-out will-change-transform',
// Collapsed: slide left so only 80px rail is visible
isCollapsed && 'md:-translate-x-[160px]',
// Prevent flash before mount
!isMounted && 'invisible',
)}
>
{sidebar}
</aside>

{/* Toggle button in rail */}
<button
onClick={toggleSidebar}
className={cn(
'absolute bottom-2 z-10 flex h-8 w-8 items-center justify-center rounded-xs transition-all hover:bg-accent',
isCollapsed ? 'left-1/2 -translate-x-1/2' : 'right-2',
)}
aria-label="Toggle sidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn(
'transition-transform duration-200 ease-in-out',
isCollapsed && 'rotate-180',
)}
>
<path d="m9 18 6-6-6-6" />
</svg>
</button>
</div>

{/* Main Content */}
<main className="bg-background overflow-y-auto">{children}</main>
</div>
);
}
Loading
Loading