diff --git a/apps/web/src/app/api/messages/conversations/route.ts b/apps/web/src/app/api/messages/conversations/route.ts
index fcd99a57f6..4c65a1c49f 100644
--- a/apps/web/src/app/api/messages/conversations/route.ts
+++ b/apps/web/src/app/api/messages/conversations/route.ts
@@ -21,75 +21,75 @@ export async function GET(request: Request) {
WITH conversation_data AS (
SELECT
c.id,
- c.participant1_id,
- c.participant2_id,
- c.last_message_at,
- c.last_message_preview,
- c.participant1_last_read,
- c.participant2_last_read,
- c.created_at,
+ c."participant1Id",
+ c."participant2Id",
+ c."lastMessageAt",
+ c."lastMessagePreview",
+ c."participant1LastRead",
+ c."participant2LastRead",
+ c."createdAt",
CASE
- WHEN c.participant1_id = ${user.id} THEN c.participant2_id
- ELSE c.participant1_id
+ WHEN c."participant1Id" = ${user.id} THEN c."participant2Id"
+ ELSE c."participant1Id"
END as other_user_id,
CASE
- WHEN c.participant1_id = ${user.id} THEN c.participant1_last_read
- ELSE c.participant2_last_read
+ WHEN c."participant1Id" = ${user.id} THEN c."participant1LastRead"
+ ELSE c."participant2LastRead"
END as last_read
FROM dm_conversations c
- WHERE c.participant1_id = ${user.id} OR c.participant2_id = ${user.id}
+ WHERE c."participant1Id" = ${user.id} OR c."participant2Id" = ${user.id}
${cursor ? (direction === 'before'
- ? sql`AND c.last_message_at > ${cursor}`
- : sql`AND c.last_message_at < ${cursor}`)
+ ? sql`AND c."lastMessageAt" > ${cursor}`
+ : sql`AND c."lastMessageAt" < ${cursor}`)
: sql``}
- ORDER BY c.last_message_at DESC NULLS LAST
+ ORDER BY c."lastMessageAt" DESC NULLS LAST
LIMIT ${limit}
),
unread_counts AS (
SELECT
- dm.conversation_id,
+ dm."conversationId",
COUNT(*) as unread_count
FROM direct_messages dm
- INNER JOIN conversation_data cd ON dm.conversation_id = cd.id
- WHERE dm.sender_id = cd.other_user_id
- AND dm.is_read = false
- GROUP BY dm.conversation_id
+ INNER JOIN conversation_data cd ON dm."conversationId" = cd.id
+ WHERE dm."senderId" = cd.other_user_id
+ AND dm."isRead" = false
+ GROUP BY dm."conversationId"
)
SELECT
cd.id,
- cd.participant1_id,
- cd.participant2_id,
- cd.last_message_at,
- cd.last_message_preview,
- cd.participant1_last_read,
- cd.participant2_last_read,
- cd.created_at,
+ cd."participant1Id",
+ cd."participant2Id",
+ cd."lastMessageAt",
+ cd."lastMessagePreview",
+ cd."participant1LastRead",
+ cd."participant2LastRead",
+ cd."createdAt",
cd.last_read,
u.id as other_user_id,
u.name as other_user_name,
u.email as other_user_email,
u.image as other_user_image,
up.username as other_user_username,
- up.display_name as other_user_display_name,
- up.avatar_url as other_user_avatar_url,
+ up."displayName" as other_user_display_name,
+ up."avatarUrl" as other_user_avatar_url,
COALESCE(uc.unread_count, 0) as unread_count
FROM conversation_data cd
LEFT JOIN users u ON u.id = cd.other_user_id
- LEFT JOIN user_profiles up ON up.user_id = cd.other_user_id
- LEFT JOIN unread_counts uc ON uc.conversation_id = cd.id
- ORDER BY cd.last_message_at DESC NULLS LAST
+ LEFT JOIN user_profiles up ON up."userId" = cd.other_user_id
+ LEFT JOIN unread_counts uc ON uc."conversationId" = cd.id
+ ORDER BY cd."lastMessageAt" DESC NULLS LAST
`);
// Transform the raw results to match the expected format
interface ConversationRow {
id: string;
- participant1_id: string;
- participant2_id: string;
- last_message_at: string | null;
- last_message_preview: string | null;
- participant1_last_read: string | null;
- participant2_last_read: string | null;
- created_at: string;
+ participant1Id: string;
+ participant2Id: string;
+ lastMessageAt: string | null;
+ lastMessagePreview: string | null;
+ participant1LastRead: string | null;
+ participant2LastRead: string | null;
+ createdAt: string;
last_read: string | null;
other_user_id: string;
other_user_name: string;
@@ -105,13 +105,13 @@ export async function GET(request: Request) {
const typedRow = row as unknown as ConversationRow;
return {
id: typedRow.id,
- participant1Id: typedRow.participant1_id,
- participant2Id: typedRow.participant2_id,
- lastMessageAt: typedRow.last_message_at,
- lastMessagePreview: typedRow.last_message_preview,
- participant1LastRead: typedRow.participant1_last_read,
- participant2LastRead: typedRow.participant2_last_read,
- createdAt: typedRow.created_at,
+ participant1Id: typedRow.participant1Id,
+ participant2Id: typedRow.participant2Id,
+ lastMessageAt: typedRow.lastMessageAt,
+ lastMessagePreview: typedRow.lastMessagePreview,
+ participant1LastRead: typedRow.participant1LastRead,
+ participant2LastRead: typedRow.participant2LastRead,
+ createdAt: typedRow.createdAt,
lastRead: typedRow.last_read,
otherUser: {
id: typedRow.other_user_id,
diff --git a/apps/web/src/app/dashboard/storage/page.tsx b/apps/web/src/app/dashboard/storage/page.tsx
index 6c6179f4a3..ed4452ac37 100644
--- a/apps/web/src/app/dashboard/storage/page.tsx
+++ b/apps/web/src/app/dashboard/storage/page.tsx
@@ -142,7 +142,7 @@ export default function StorageDashboard() {
if (loading) {
return (
-
+
@@ -157,7 +157,7 @@ export default function StorageDashboard() {
if (!storageInfo) {
return (
-
+
Error
@@ -171,7 +171,7 @@ export default function StorageDashboard() {
return (
-
+
Back to Dashboard
-
+
Storage Dashboard
@@ -194,6 +194,7 @@ export default function StorageDashboard() {
disabled={reconciling}
variant="outline"
size="sm"
+ className="w-full sm:w-auto"
>
Reconcile Storage
@@ -233,7 +234,7 @@ export default function StorageDashboard() {
value={quota.utilizationPercent}
className="h-3"
/>
-
+
{quota.formattedAvailable} available
@@ -247,7 +248,7 @@ export default function StorageDashboard() {
{/* Tier Information Card */}
-
+
Storage Plan
{tierInfo.name}
@@ -257,18 +258,18 @@ export default function StorageDashboard() {
-
+
Total Files:
{totalFiles}
{tierInfo.maxFileCount > 0 && ` / ${tierInfo.maxFileCount}`}
-
+
Max File Size:
{formatBytes(tierInfo.maxFileSize)}
-
+
Concurrent Uploads:
{tierInfo.maxConcurrentUploads}
@@ -318,28 +319,30 @@ export default function StorageDashboard() {
Storage usage across your drives
-
-
-
- Drive Name
- Files
- Storage Used
- Percentage
-
-
-
- {storageByDrive.map((drive) => (
-
- {drive.driveName}
- {drive.fileCount}
- {drive.formattedSize}
-
- {Math.round((drive.totalSize / quota.usedBytes) * 100)}%
-
+
+
+
+
+ Drive Name
+ Files
+ Storage Used
+ Percentage
- ))}
-
-
+
+
+ {storageByDrive.map((drive) => (
+
+ {drive.driveName}
+ {drive.fileCount}
+ {drive.formattedSize}
+
+ {Math.round((drive.totalSize / quota.usedBytes) * 100)}%
+
+
+ ))}
+
+
+
)}
diff --git a/apps/web/src/app/settings/account/page.tsx b/apps/web/src/app/settings/account/page.tsx
new file mode 100644
index 0000000000..fc3e9b2b40
--- /dev/null
+++ b/apps/web/src/app/settings/account/page.tsx
@@ -0,0 +1,461 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useAuth } from "@/hooks/use-auth";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { toast } from "sonner";
+import { User, Mail, Calendar, AlertTriangle, Loader2, ArrowLeft, Upload, X } from "lucide-react";
+
+export default function AccountPage() {
+ const { user, isLoading: authLoading, mutate } = useAuth();
+ const router = useRouter();
+
+ // Profile form state
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [isSavingProfile, setIsSavingProfile] = useState(false);
+
+ // Avatar state
+ const [avatarFile, setAvatarFile] = useState
(null);
+ const [avatarPreview, setAvatarPreview] = useState(null);
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
+
+ // Password form state
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [isSavingPassword, setIsSavingPassword] = useState(false);
+
+ // Load user data into form
+ useEffect(() => {
+ if (user) {
+ setName(user.name || "");
+ setEmail(user.email || "");
+ if (user.image) {
+ setAvatarPreview(user.image);
+ }
+ }
+ }, [user]);
+
+ // Redirect if not authenticated
+ useEffect(() => {
+ if (!authLoading && !user) {
+ router.push("/auth/signin");
+ }
+ }, [authLoading, user, router]);
+
+ const handleProfileUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSavingProfile(true);
+
+ try {
+ const response = await fetch("/api/account", {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ name, email }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to update profile");
+ }
+
+ if (mutate) {
+ await mutate(); // Refresh user data
+ }
+ toast.success("Profile updated successfully");
+ } catch (error) {
+ console.error("Profile update error:", error);
+ toast.error(error instanceof Error ? error.message : "Failed to update profile");
+ } finally {
+ setIsSavingProfile(false);
+ }
+ };
+
+ const handleAvatarSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Validate file type
+ if (!file.type.startsWith('image/')) {
+ toast.error('Please select an image file');
+ return;
+ }
+
+ // Validate file size (5MB)
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image must be less than 5MB');
+ return;
+ }
+
+ setAvatarFile(file);
+
+ // Create preview
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setAvatarPreview(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleAvatarUpload = async () => {
+ if (!avatarFile) return;
+
+ setIsUploadingAvatar(true);
+ const formData = new FormData();
+ formData.append('file', avatarFile);
+
+ try {
+ const response = await fetch('/api/account/avatar', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to upload avatar');
+ }
+
+ await response.json();
+ toast.success('Avatar uploaded successfully');
+ setAvatarFile(null);
+
+ // Refresh user data to get new avatar URL
+ if (mutate) {
+ await mutate();
+ }
+ } catch (error) {
+ console.error('Avatar upload error:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to upload avatar');
+ } finally {
+ setIsUploadingAvatar(false);
+ }
+ };
+
+ const handleAvatarDelete = async () => {
+ setIsUploadingAvatar(true);
+
+ try {
+ const response = await fetch('/api/account/avatar', {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to delete avatar');
+ }
+
+ toast.success('Avatar deleted successfully');
+ setAvatarPreview(null);
+ setAvatarFile(null);
+
+ // Refresh user data
+ if (mutate) {
+ await mutate();
+ }
+ } catch (error) {
+ console.error('Avatar deletion error:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to delete avatar');
+ } finally {
+ setIsUploadingAvatar(false);
+ }
+ };
+
+ const handlePasswordChange = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (newPassword !== confirmPassword) {
+ toast.error("New passwords don't match");
+ return;
+ }
+
+ if (newPassword.length < 8) {
+ toast.error("Password must be at least 8 characters long");
+ return;
+ }
+
+ setIsSavingPassword(true);
+
+ try {
+ const response = await fetch("/api/account/password", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ currentPassword,
+ newPassword,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to change password");
+ }
+
+ toast.success("Password changed successfully");
+ // Clear password fields
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ } catch (error) {
+ console.error("Password change error:", error);
+ toast.error(error instanceof Error ? error.message : "Failed to change password");
+ } finally {
+ setIsSavingPassword(false);
+ }
+ };
+
+ if (authLoading || !user) {
+ return (
+
+
+
+ );
+ }
+
+ const memberSince = user.id ? new Date().toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ }) : "Unknown";
+
+ return (
+
+
+
router.push('/settings')}
+ className="mb-4"
+ >
+
+ Back to Settings
+
+
Account Settings
+
+ Manage your account information and security settings
+
+
+
+ {/* Profile Section */}
+
+
+ Profile Information
+ Update your personal information
+
+
+
+
+
+
+ {/* Security Section */}
+
+
+ Security
+ Manage your password and security settings
+
+
+
+
+
+
+ {/* Account Info */}
+
+
+ Account Information
+ View your account details
+
+
+
+
+ Member since:
+ {memberSince}
+
+
+
+ Account ID:
+ {user.id}
+
+
+
+
+ {/* Danger Zone */}
+
+
+ Danger Zone
+ Irreversible actions for your account
+
+
+
+
+
+ Account deletion is permanent and cannot be undone. This feature is coming soon.
+
+
+
+ Delete Account (Coming Soon)
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/settings/ai/page.tsx b/apps/web/src/app/settings/ai/page.tsx
index 3fffcac46d..0ff58a7a2f 100644
--- a/apps/web/src/app/settings/ai/page.tsx
+++ b/apps/web/src/app/settings/ai/page.tsx
@@ -279,7 +279,7 @@ export default function AiSettingsPage() {
-
+
OpenRouter
{isProviderConfigured('openrouter') ? (
diff --git a/apps/web/src/app/settings/layout.tsx b/apps/web/src/app/settings/layout.tsx
index b6b33b280e..1dae82f3cc 100644
--- a/apps/web/src/app/settings/layout.tsx
+++ b/apps/web/src/app/settings/layout.tsx
@@ -1,53 +1,7 @@
"use client";
-import { useAuth } from "@/hooks/use-auth";
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import TopBar from "@/components/layout/main-header";
-import Sidebar from "@/components/layout/left-sidebar";
-import RightPanel from "@/components/layout/right-sidebar";
-import { useLayoutStore } from "@/stores/useLayoutStore";
-import { useHasHydrated } from "@/hooks/useHasHydrated";
+import Layout from "@/components/layout/Layout";
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
- const { isLoading, isAuthenticated } = useAuth();
- const router = useRouter();
- const {
- leftSidebarOpen,
- rightSidebarOpen,
- toggleLeftSidebar,
- toggleRightSidebar,
- } = useLayoutStore();
- const hasHydrated = useHasHydrated();
-
- useEffect(() => {
- if (!isLoading && !isAuthenticated) {
- router.push('/auth/signin');
- }
- }, [isLoading, isAuthenticated, router]);
-
- if (isLoading || !hasHydrated) {
- return
Loading...
;
- }
-
- if (!isAuthenticated) {
- router.push("/auth/signin");
- return null;
- }
-
- return (
-
-
-
- {leftSidebarOpen && }
-
- {children}
-
- {rightSidebarOpen && }
-
-
- );
-}
\ No newline at end of file
+ return
{children} ;
+}
diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx
index 9dc34a32dc..d4f3a3e461 100644
--- a/apps/web/src/app/settings/page.tsx
+++ b/apps/web/src/app/settings/page.tsx
@@ -20,7 +20,7 @@ export default function SettingsPage() {
title: "Account",
description: "Manage your account and profile",
icon: User,
- href: "/account",
+ href: "/settings/account",
available: true,
},
{
@@ -47,7 +47,7 @@ export default function SettingsPage() {
];
return (
-
+
-
+
{settingsCategories.map((category) => (
Content Statistics
-
-
-
- Drives
- {user.stats.drives}
-
-
- Pages
- {user.stats.pages}
-
-
- Page Messages
- {user.stats.chatMessages}
-
-
- Drive Messages
- {user.stats.driveChatMessages}
-
-
- Global Messages
- {user.stats.globalMessages}
-
-
- Active Sessions
- {user.stats.refreshTokens}
-
-
-
+
+
+
+
+ Drives
+ {user.stats.drives}
+
+
+ Pages
+ {user.stats.pages}
+
+
+ Page Messages
+ {user.stats.chatMessages}
+
+
+ Drive Messages
+ {user.stats.driveChatMessages}
+
+
+ Global Messages
+ {user.stats.globalMessages}
+
+
+ Active Sessions
+ {user.stats.refreshTokens}
+
+
+
+
{/* AI Settings and Account Info */}
diff --git a/apps/web/src/components/billing/UsageCounter.tsx b/apps/web/src/components/billing/UsageCounter.tsx
index a4745cc46d..3bc784c79e 100644
--- a/apps/web/src/components/billing/UsageCounter.tsx
+++ b/apps/web/src/components/billing/UsageCounter.tsx
@@ -108,7 +108,7 @@ export function UsageCounter() {
}
return (
-
+
{/* Usage Display */}
diff --git a/apps/web/src/components/layout/Layout.tsx b/apps/web/src/components/layout/Layout.tsx
index da4cea22da..2f24adb22a 100644
--- a/apps/web/src/components/layout/Layout.tsx
+++ b/apps/web/src/components/layout/Layout.tsx
@@ -7,13 +7,15 @@ import CenterPanel from "@/components/layout/middle-content/CenterPanel";
import MemoizedRightPanel from "@/components/layout/right-sidebar/MemoizedRightPanel";
import { NavigationProvider } from "@/components/layout/NavigationProvider";
import { useMobile } from "@/hooks/use-mobile";
+import { useBreakpoint } from "@/hooks/use-breakpoint";
+import { useResponsivePanels } from "@/hooks/use-responsive-panels";
import { motion, AnimatePresence } from "motion/react";
import { DebugPanel } from "./DebugPanel";
import { useLayoutStore } from "@/stores/useLayoutStore";
import { useHasHydrated } from "@/hooks/useHasHydrated";
import { usePerformanceMonitor } from "@/hooks/usePerformanceMonitor";
import { useRouter } from "next/navigation";
-import { useEffect, memo } from "react";
+import { useCallback, useEffect, memo } from "react";
interface LayoutProps {
children?: React.ReactNode;
@@ -28,8 +30,13 @@ function Layout({ children }: LayoutProps) {
rightSidebarOpen,
toggleLeftSidebar,
toggleRightSidebar,
+ setLeftSidebarOpen,
+ setRightSidebarOpen,
} = useLayoutStore();
const hasHydrated = useHasHydrated();
+ const shouldOverlaySidebars = useBreakpoint("(max-width: 1279px)");
+
+ useResponsivePanels();
// Monitor performance
@@ -61,6 +68,52 @@ function Layout({ children }: LayoutProps) {
}
}, [hasHydrated, isLoading, isAuthenticated, router]);
+ const handleLeftPanelToggle = useCallback(() => {
+ if (shouldOverlaySidebars) {
+ if (leftSidebarOpen) {
+ setLeftSidebarOpen(false);
+ } else {
+ if (rightSidebarOpen) {
+ setRightSidebarOpen(false);
+ }
+ setLeftSidebarOpen(true);
+ }
+ return;
+ }
+
+ toggleLeftSidebar();
+ }, [
+ shouldOverlaySidebars,
+ leftSidebarOpen,
+ rightSidebarOpen,
+ setLeftSidebarOpen,
+ setRightSidebarOpen,
+ toggleLeftSidebar,
+ ]);
+
+ const handleRightPanelToggle = useCallback(() => {
+ if (shouldOverlaySidebars) {
+ if (rightSidebarOpen) {
+ setRightSidebarOpen(false);
+ } else {
+ if (leftSidebarOpen) {
+ setLeftSidebarOpen(false);
+ }
+ setRightSidebarOpen(true);
+ }
+ return;
+ }
+
+ toggleRightSidebar();
+ }, [
+ shouldOverlaySidebars,
+ leftSidebarOpen,
+ rightSidebarOpen,
+ setLeftSidebarOpen,
+ setRightSidebarOpen,
+ toggleRightSidebar,
+ ]);
+
// Optimize loading checks - show UI earlier for better perceived performance
if (isLoading || !hasHydrated) {
return (
@@ -87,91 +140,93 @@ function Layout({ children }: LayoutProps) {
return (
-
+
-
-
- {/* Desktop Left Sidebar */}
- {leftSidebarOpen && !isMobile && (
-
-
+
+
+ {!shouldOverlaySidebars && leftSidebarOpen && (
+
+
)}
- {/* Mobile Left Sidebar */}
- {isMobile && leftSidebarOpen && (
- <>
-
-
-
-
- {/* Mobile Overlay */}
-
- >
+ {shouldOverlaySidebars && leftSidebarOpen && (
+
+
+
+
+
)}
- {/* Main Content Area */}
-
- {/* Use our center panel instead of children */}
- {children || }
+
+ {children ? (
+
+ {children}
+
+ ) : (
+
+ )}
- {/* Desktop Right Sidebar */}
- {rightSidebarOpen && !isMobile && (
-
-
+ {!shouldOverlaySidebars && rightSidebarOpen && (
+
+
)}
- {/* Mobile Right Sidebar */}
- {isMobile && rightSidebarOpen && (
- <>
-
-
-
-
- {/* Mobile Overlay (if not already shown for left sidebar) */}
- {!leftSidebarOpen && (
-
- )}
- >
+ {shouldOverlaySidebars && rightSidebarOpen && (
+
+
+
+
+
+ )}
+
+
+
+ {shouldOverlaySidebars && (leftSidebarOpen || rightSidebarOpen) && (
+ {
+ if (leftSidebarOpen) {
+ setLeftSidebarOpen(false);
+ }
+ if (rightSidebarOpen) {
+ setRightSidebarOpen(false);
+ }
+ }}
+ />
)}
-
- {/* Debug Panel for Development */}
+
diff --git a/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx b/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx
index 4c412b72ea..18f23b6c13 100644
--- a/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx
+++ b/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx
@@ -2,7 +2,7 @@
import { memo } from 'react';
import { usePathname } from 'next/navigation';
-import Sidebar from './index';
+import Sidebar, { type SidebarProps } from './index';
import MessagesLeftSidebar from './MessagesLeftSidebar';
/**
@@ -10,15 +10,15 @@ import MessagesLeftSidebar from './MessagesLeftSidebar';
* This helps avoid infinite loops in the layout system
* Renders MessagesLeftSidebar when on messages route
*/
-const MemoizedSidebar = memo(() => {
+const MemoizedSidebar = memo((props: SidebarProps) => {
const pathname = usePathname();
const isMessagesRoute = pathname?.startsWith('/dashboard/messages');
if (isMessagesRoute) {
- return
;
+ return
;
}
- return
;
+ return
;
});
MemoizedSidebar.displayName = 'MemoizedSidebar';
diff --git a/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx b/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx
index 7029562a95..7f2053a292 100644
--- a/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx
+++ b/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx
@@ -11,6 +11,7 @@ import { formatDistanceToNow } from 'date-fns';
import useSWR from 'swr';
import Link from 'next/link';
import { cn } from '@/lib/utils';
+import type { SidebarProps } from './index';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -38,7 +39,7 @@ interface ConversationsResponse {
};
}
-export default function MessagesLeftSidebar() {
+export default function MessagesLeftSidebar({ className, variant = 'desktop' }: SidebarProps) {
const router = useRouter();
const params = useParams();
const [searchQuery, setSearchQuery] = useState('');
@@ -89,8 +90,14 @@ export default function MessagesLeftSidebar() {
};
return (
-
-
+
+
{/* Header */}
diff --git a/apps/web/src/components/layout/left-sidebar/index.tsx b/apps/web/src/components/layout/left-sidebar/index.tsx
index 0a8308d68f..b459d294cd 100644
--- a/apps/web/src/components/layout/left-sidebar/index.tsx
+++ b/apps/web/src/components/layout/left-sidebar/index.tsx
@@ -1,128 +1,169 @@
-'use client';
+"use client";
-import PageTree from "./page-tree/PageTree";
-import { Button } from "@/components/ui/button";
-import { Trash2, Search, Plus, Users, Lock, Settings, HardDrive } from "lucide-react";
-import { useState, useEffect } from "react";
-import CreatePageDialog from "./CreatePageDialog";
-import { Input } from "@/components/ui/input";
+import { useEffect, useState } from "react";
+import Link from "next/link";
import { useParams } from "next/navigation";
import { useSWRConfig } from "swr";
-import DriveList from "./DriveList";
-import Link from "next/link";
-import DriveSwitcher from "./workspace-selector";
-import { useAuth } from "@/hooks/use-auth";
-import { getPermissionErrorMessage } from "@/hooks/use-permissions";
-import { useDriveStore } from "@/hooks/useDrive";
+import {
+ HardDrive,
+ Lock,
+ Plus,
+ Search,
+ Settings,
+ Trash2,
+ Users,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import { useAuth } from "@/hooks/use-auth";
+import { getPermissionErrorMessage } from "@/hooks/use-permissions";
+import { useDriveStore } from "@/hooks/useDrive";
+
+import CreatePageDialog from "./CreatePageDialog";
+import DriveList from "./DriveList";
+import DriveSwitcher from "./workspace-selector";
+import PageTree from "./page-tree/PageTree";
-export default function Sidebar() {
- const [isCreatePageOpen, setCreatePageOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState('');
- const params = useParams();
- const { driveId: driveIdParams } = params;
- const { user } = useAuth();
- const { drives, fetchDrives } = useDriveStore();
+export interface SidebarProps {
+ className?: string;
+ variant?: "desktop" | "overlay";
+}
- const driveId = Array.isArray(driveIdParams) ? driveIdParams[0] : driveIdParams;
- const { mutate } = useSWRConfig();
+export default function Sidebar({ className, variant = "desktop" }: SidebarProps) {
+ const [isCreatePageOpen, setCreatePageOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const params = useParams();
+ const { driveId: driveIdParams } = params;
+ const { user } = useAuth();
+ const { drives, fetchDrives } = useDriveStore();
- // Get drive from store and check if user can create pages
- const drive = drives.find(d => d.id === driveId);
- const canCreatePages = drive?.isOwned || false;
+ const driveId = Array.isArray(driveIdParams) ? driveIdParams[0] : driveIdParams;
+ const { mutate } = useSWRConfig();
- // Fetch drives if not already loaded
- useEffect(() => {
- if (driveId && user?.id) {
- fetchDrives();
- }
- }, [driveId, user?.id, fetchDrives]);
+ const drive = drives.find((d) => d.id === driveId);
+ const canCreatePages = drive?.isOwned || false;
- const handlePageCreated = () => {
- if (driveId) {
- mutate(`/api/drives/${driveId}/pages`);
- }
- };
+ useEffect(() => {
+ if (driveId && user?.id) {
+ fetchDrives();
+ }
+ }, [driveId, user?.id, fetchDrives]);
+
+ const handlePageCreated = () => {
+ if (driveId) {
+ void mutate(`/api/drives/${driveId}/pages`);
+ }
+ };
return (
-
-
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/components/layout/main-header/index.tsx b/apps/web/src/components/layout/main-header/index.tsx
index dc1d0d1042..25981c8fa5 100644
--- a/apps/web/src/components/layout/main-header/index.tsx
+++ b/apps/web/src/components/layout/main-header/index.tsx
@@ -1,13 +1,15 @@
-import Link from 'next/link';
-import { Home, PanelLeft, PanelRight, Search } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { useMobile } from '@/hooks/use-mobile';
-import UserDropdown from '@/components/shared/UserDropdown';
-import NotificationBell from '@/components/notifications/NotificationBell';
-import InlineSearch from '@/components/search/InlineSearch';
-import GlobalSearch from '@/components/search/GlobalSearch';
-import { UsageCounter } from '@/components/billing/UsageCounter';
-import { useState } from 'react';
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Crown, Home, PanelLeft, PanelRight, Search } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import NotificationBell from "@/components/notifications/NotificationBell";
+import InlineSearch from "@/components/search/InlineSearch";
+import GlobalSearch from "@/components/search/GlobalSearch";
+import UserDropdown from "@/components/shared/UserDropdown";
+import { UsageCounter } from "@/components/billing/UsageCounter";
interface TopBarProps {
onToggleLeftPanel: () => void;
@@ -15,63 +17,94 @@ interface TopBarProps {
}
export default function TopBar({ onToggleLeftPanel, onToggleRightPanel }: TopBarProps) {
- const isMobile = useMobile();
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
return (
- <>
-
- {/* Panel Toggles, Breadcrumbs, and Global Search */}
-
+
+
+
+
+
+
+
-
-
+
+
+
/
- {/* Desktop search - inline functional input */}
-
+
+
- {/* Mobile search - opens full dialog */}
+
setMobileSearchOpen(true)}
className="md:hidden"
+ aria-label="Open search"
>
- {/* User Controls */}
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {/* Mobile Search Dialog */}
- >
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/components/layout/middle-content/page-views/dashboard/GlobalAssistantView.tsx b/apps/web/src/components/layout/middle-content/page-views/dashboard/GlobalAssistantView.tsx
index b047472e4e..b8fe4bcf8a 100644
--- a/apps/web/src/components/layout/middle-content/page-views/dashboard/GlobalAssistantView.tsx
+++ b/apps/web/src/components/layout/middle-content/page-views/dashboard/GlobalAssistantView.tsx
@@ -566,7 +566,7 @@ const GlobalAssistantView: React.FC = () => {
{/* Messages Area */}
-
+
diff --git a/apps/web/src/components/layout/right-sidebar/MemoizedRightPanel.tsx b/apps/web/src/components/layout/right-sidebar/MemoizedRightPanel.tsx
index a39b601b6d..5736cb8b72 100644
--- a/apps/web/src/components/layout/right-sidebar/MemoizedRightPanel.tsx
+++ b/apps/web/src/components/layout/right-sidebar/MemoizedRightPanel.tsx
@@ -1,14 +1,14 @@
'use client';
import { memo } from 'react';
-import RightPanel from './index';
+import RightPanel, { type RightPanelProps } from './index';
/**
* Memoized version of RightPanel to prevent unnecessary re-renders
* This helps avoid infinite loops in the layout system
*/
-const MemoizedRightPanel = memo(() => {
- return ;
+const MemoizedRightPanel = memo((props: RightPanelProps) => {
+ return ;
});
MemoizedRightPanel.displayName = 'MemoizedRightPanel';
diff --git a/apps/web/src/components/layout/right-sidebar/ai-assistant/AssistantChatTab.tsx b/apps/web/src/components/layout/right-sidebar/ai-assistant/AssistantChatTab.tsx
index be4faac2b4..e6c7e063c4 100644
--- a/apps/web/src/components/layout/right-sidebar/ai-assistant/AssistantChatTab.tsx
+++ b/apps/web/src/components/layout/right-sidebar/ai-assistant/AssistantChatTab.tsx
@@ -500,7 +500,7 @@ const AssistantChatTab: React.FC = () => {
{/* Messages Area */}
-
+
{/* Load More Messages Button */}
diff --git a/apps/web/src/components/layout/right-sidebar/index.tsx b/apps/web/src/components/layout/right-sidebar/index.tsx
index 52d16039e5..d5d7f0ad11 100644
--- a/apps/web/src/components/layout/right-sidebar/index.tsx
+++ b/apps/web/src/components/layout/right-sidebar/index.tsx
@@ -1,143 +1,161 @@
-'use client';
+"use client";
-import { useState, useEffect } from 'react';
-import { usePathname } from 'next/navigation';
-import { MessageSquare, History, Settings } from 'lucide-react';
-import AssistantChatTab from './ai-assistant/AssistantChatTab';
-import AssistantHistoryTab from './ai-assistant/AssistantHistoryTab';
-import AssistantSettingsTab from './ai-assistant/AssistantSettingsTab';
-import { createClientLogger } from '@/lib/logging/client-logger';
+import { useEffect, useState } from "react";
+import { usePathname } from "next/navigation";
+import { History, MessageSquare, Settings } from "lucide-react";
-const panelLogger = createClientLogger({ namespace: 'ui', component: 'right-sidebar' });
+import { cn } from "@/lib/utils";
+import { createClientLogger } from "@/lib/logging/client-logger";
-export default function RightPanel() {
+import AssistantChatTab from "./ai-assistant/AssistantChatTab";
+import AssistantHistoryTab from "./ai-assistant/AssistantHistoryTab";
+import AssistantSettingsTab from "./ai-assistant/AssistantSettingsTab";
+
+export interface RightPanelProps {
+ className?: string;
+ variant?: "desktop" | "overlay";
+}
+
+const panelLogger = createClientLogger({ namespace: "ui", component: "right-sidebar" });
+
+export default function RightPanel({ className, variant = "desktop" }: RightPanelProps) {
const pathname = usePathname();
- panelLogger.debug('Evaluating RightPanel pathname', {
+ panelLogger.debug("Evaluating RightPanel pathname", {
pathname,
pathnameType: typeof pathname,
});
- // Treat dashboard and drive root pages the same (no chat tab)
let isDashboardOrDrive = false;
- // Safely handle pathname that might be null/undefined
- if (pathname && typeof pathname === 'string') {
+ if (pathname && typeof pathname === "string") {
try {
const matchResult = pathname.match(/^\/dashboard\/[^/]+$/);
- panelLogger.debug('RightPanel pathname match evaluated', {
+ panelLogger.debug("RightPanel pathname match evaluated", {
matchFound: Boolean(matchResult),
});
- isDashboardOrDrive = pathname === '/dashboard' || !!matchResult;
+ isDashboardOrDrive = pathname === "/dashboard" || Boolean(matchResult);
} catch (error) {
- panelLogger.error('Failed to evaluate pathname match in RightPanel', {
+ panelLogger.error("Failed to evaluate pathname match in RightPanel", {
error: error instanceof Error ? error : String(error),
});
isDashboardOrDrive = false;
}
} else {
- panelLogger.warn('RightPanel received null or undefined pathname');
+ panelLogger.warn("RightPanel received null or undefined pathname");
isDashboardOrDrive = false;
}
- panelLogger.debug('RightPanel computed dashboard/drive state', {
+ panelLogger.debug("RightPanel computed dashboard/drive state", {
isDashboardOrDrive,
});
-
- // Determine default tab based on context
- const defaultTab = isDashboardOrDrive ? 'history' : 'chat';
+
+ const defaultTab = isDashboardOrDrive ? "history" : "chat";
const [activeTab, setActiveTab] = useState
(defaultTab);
- // Remember tab state across sessions, but respect context
useEffect(() => {
- const savedTab = localStorage.getItem('globalAssistantActiveTab');
-
- // If on dashboard or drive root, don't allow 'chat' tab
- if (isDashboardOrDrive && savedTab === 'chat') {
- setActiveTab('history');
- } else if (savedTab && ['chat', 'history', 'settings'].includes(savedTab)) {
- // Only set saved tab if it's valid for current context
- if (!isDashboardOrDrive || savedTab !== 'chat') {
+ const savedTab = localStorage.getItem("globalAssistantActiveTab");
+
+ if (isDashboardOrDrive && savedTab === "chat") {
+ setActiveTab("history");
+ } else if (savedTab && ["chat", "history", "settings"].includes(savedTab)) {
+ if (!isDashboardOrDrive || savedTab !== "chat") {
setActiveTab(savedTab);
}
}
}, [isDashboardOrDrive]);
- // Listen for storage events to update tab when buttons are clicked
useEffect(() => {
const handleStorageChange = () => {
- const savedTab = localStorage.getItem('globalAssistantActiveTab');
- if (savedTab && ['history', 'settings'].includes(savedTab)) {
+ const savedTab = localStorage.getItem("globalAssistantActiveTab");
+ if (savedTab && ["history", "settings"].includes(savedTab)) {
setActiveTab(savedTab);
}
};
- window.addEventListener('storage', handleStorageChange);
- return () => window.removeEventListener('storage', handleStorageChange);
+ window.addEventListener("storage", handleStorageChange);
+ return () => window.removeEventListener("storage", handleStorageChange);
}, []);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
- localStorage.setItem('globalAssistantActiveTab', tab);
+ localStorage.setItem("globalAssistantActiveTab", tab);
};
return (
-
- {/* Custom Tab Header */}
+
-
- {/* Only show Chat tab when not on dashboard or drive root */}
+
{!isDashboardOrDrive && (
handleTabChange('chat')}
- className={`flex items-center justify-center space-x-1 py-3 text-sm font-medium transition-colors relative
- ${activeTab === 'chat'
- ? 'text-foreground bg-background'
- : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}`}
+ onClick={() => handleTabChange("chat")}
+ className={cn(
+ "relative flex items-center justify-center gap-1 rounded-md px-2 py-2 transition-colors",
+ activeTab === "chat"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
+ )}
+ aria-pressed={activeTab === "chat"}
>
Chat
- {activeTab === 'chat' && (
-
+ {activeTab === "chat" && (
+
)}
)}
-
+
handleTabChange('history')}
- className={`flex items-center justify-center space-x-1 py-3 text-sm font-medium transition-colors relative
- ${activeTab === 'history'
- ? 'text-foreground bg-background'
- : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}`}
+ onClick={() => handleTabChange("history")}
+ className={cn(
+ "relative flex items-center justify-center gap-1 rounded-md px-2 py-2 transition-colors",
+ activeTab === "history"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
+ )}
+ aria-pressed={activeTab === "history"}
>
History
- {activeTab === 'history' && (
-
+ {activeTab === "history" && (
+
)}
-
+
handleTabChange('settings')}
- className={`flex items-center justify-center space-x-1 py-3 text-sm font-medium transition-colors relative
- ${activeTab === 'settings'
- ? 'text-foreground bg-background'
- : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}`}
+ onClick={() => handleTabChange("settings")}
+ className={cn(
+ "relative flex items-center justify-center gap-1 rounded-md px-2 py-2 transition-colors",
+ activeTab === "settings"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
+ )}
+ aria-pressed={activeTab === "settings"}
>
Settings
- {activeTab === 'settings' && (
-
+ {activeTab === "settings" && (
+
)}
-
- {/* Tab Content - Simple conditional rendering */}
-
- {activeTab === 'chat' &&
}
- {activeTab === 'history' &&
}
- {activeTab === 'settings' &&
}
+
+
+ {activeTab === "chat" &&
}
+ {activeTab === "history" &&
}
+ {activeTab === "settings" &&
}
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/components/notifications/NotificationBell.tsx b/apps/web/src/components/notifications/NotificationBell.tsx
index 3ebd33b264..9663609c7b 100644
--- a/apps/web/src/components/notifications/NotificationBell.tsx
+++ b/apps/web/src/components/notifications/NotificationBell.tsx
@@ -51,7 +51,7 @@ export default function NotificationBell() {
)}
-
+
diff --git a/apps/web/src/components/shared/UserDropdown.tsx b/apps/web/src/components/shared/UserDropdown.tsx
index 36a5f64c6b..7df799c0e0 100644
--- a/apps/web/src/components/shared/UserDropdown.tsx
+++ b/apps/web/src/components/shared/UserDropdown.tsx
@@ -108,7 +108,7 @@ export default function UserDropdown() {
Billing ({subscriptionInfo?.subscriptionTier === 'free' ? 'Free' : subscriptionInfo?.subscriptionTier === 'pro' ? 'Pro' : 'Business'})
- router.push('/account')}>
+ router.push('/settings/account')}>
Account
diff --git a/apps/web/src/hooks/use-breakpoint.ts b/apps/web/src/hooks/use-breakpoint.ts
new file mode 100644
index 0000000000..a23f9c0cd1
--- /dev/null
+++ b/apps/web/src/hooks/use-breakpoint.ts
@@ -0,0 +1,39 @@
+"use client";
+
+import { useSyncExternalStore } from "react";
+
+const noop = () => {};
+
+const createSubscription = (query: string) => {
+ return (callback: () => void) => {
+ if (typeof window === "undefined") {
+ return noop;
+ }
+
+ const mediaQueryList = window.matchMedia(query);
+ const handler = () => callback();
+
+ mediaQueryList.addEventListener("change", handler);
+
+ return () => {
+ mediaQueryList.removeEventListener("change", handler);
+ };
+ };
+};
+
+const createSnapshot = (query: string) => () => {
+ if (typeof window === "undefined") {
+ return false;
+ }
+
+ return window.matchMedia(query).matches;
+};
+
+const getServerSnapshot = () => false;
+
+export function useBreakpoint(query: string) {
+ const subscribe = createSubscription(query);
+ const getSnapshot = createSnapshot(query);
+
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
+}
diff --git a/apps/web/src/hooks/use-mobile.ts b/apps/web/src/hooks/use-mobile.ts
index 75e662a244..2042b16b05 100644
--- a/apps/web/src/hooks/use-mobile.ts
+++ b/apps/web/src/hooks/use-mobile.ts
@@ -1,26 +1,9 @@
"use client";
-import { useState, useEffect } from 'react';
+import { useBreakpoint } from "./use-breakpoint";
-export function useMobile() {
- const [isMobile, setIsMobile] = useState(false);
-
- useEffect(() => {
- const checkDevice = () => {
- setIsMobile(window.innerWidth < 768); // 768px is a common breakpoint for md
- };
-
- // Check on initial mount
- checkDevice();
+const MOBILE_QUERY = "(max-width: 767px)";
- // Add event listener for window resize
- window.addEventListener('resize', checkDevice);
-
- // Cleanup event listener on component unmount
- return () => {
- window.removeEventListener('resize', checkDevice);
- };
- }, []);
-
- return isMobile;
+export function useMobile() {
+ return useBreakpoint(MOBILE_QUERY);
}
diff --git a/apps/web/src/hooks/use-responsive-panels.ts b/apps/web/src/hooks/use-responsive-panels.ts
new file mode 100644
index 0000000000..afc9f45466
--- /dev/null
+++ b/apps/web/src/hooks/use-responsive-panels.ts
@@ -0,0 +1,29 @@
+"use client";
+
+import { useEffect } from "react";
+import { useLayoutStore } from "@/stores/useLayoutStore";
+import { useBreakpoint } from "./use-breakpoint";
+
+const CLOSE_LEFT_QUERY = "(max-width: 1023px)";
+const CLOSE_RIGHT_QUERY = "(max-width: 1279px)";
+
+export function useResponsivePanels() {
+ const shouldCloseLeft = useBreakpoint(CLOSE_LEFT_QUERY);
+ const shouldCloseRight = useBreakpoint(CLOSE_RIGHT_QUERY);
+ const leftSidebarOpen = useLayoutStore((state) => state.leftSidebarOpen);
+ const rightSidebarOpen = useLayoutStore((state) => state.rightSidebarOpen);
+ const setLeftSidebarOpen = useLayoutStore((state) => state.setLeftSidebarOpen);
+ const setRightSidebarOpen = useLayoutStore((state) => state.setRightSidebarOpen);
+
+ useEffect(() => {
+ if (shouldCloseLeft && leftSidebarOpen) {
+ setLeftSidebarOpen(false);
+ }
+ }, [shouldCloseLeft, leftSidebarOpen, setLeftSidebarOpen]);
+
+ useEffect(() => {
+ if (shouldCloseRight && rightSidebarOpen) {
+ setRightSidebarOpen(false);
+ }
+ }, [shouldCloseRight, rightSidebarOpen, setRightSidebarOpen]);
+}
diff --git a/apps/web/src/lib/canvas/sample-dashboard.html b/apps/web/src/lib/canvas/sample-dashboard.html
index 40158662ee..75e61eb50a 100644
--- a/apps/web/src/lib/canvas/sample-dashboard.html
+++ b/apps/web/src/lib/canvas/sample-dashboard.html
@@ -219,7 +219,7 @@
💬 Messages
🔗 Connections
⚙️ Settings
- 👤 Account
+ 👤 Account
@@ -269,7 +269,7 @@ GitHub
diff --git a/apps/web/src/stores/useLayoutStore.ts b/apps/web/src/stores/useLayoutStore.ts
index d14b478216..c784d4575a 100644
--- a/apps/web/src/stores/useLayoutStore.ts
+++ b/apps/web/src/stores/useLayoutStore.ts
@@ -61,6 +61,8 @@ interface LayoutState {
restoreView: (pageId: string) => void;
toggleLeftSidebar: () => void;
toggleRightSidebar: () => void;
+ setLeftSidebarOpen: (open: boolean) => void;
+ setRightSidebarOpen: (open: boolean) => void;
setTreeExpanded: (nodeId: string, expanded: boolean) => void;
setTreeScrollPosition: (position: number) => void;
updateDocument: (pageId: string, updates: Partial
) => void;
@@ -321,10 +323,18 @@ export const useLayoutStore = create()(
toggleLeftSidebar: () => {
set((state) => ({ leftSidebarOpen: !state.leftSidebarOpen }));
},
-
+
toggleRightSidebar: () => {
set((state) => ({ rightSidebarOpen: !state.rightSidebarOpen }));
},
+
+ setLeftSidebarOpen: (open: boolean) => {
+ set({ leftSidebarOpen: open });
+ },
+
+ setRightSidebarOpen: (open: boolean) => {
+ set({ rightSidebarOpen: open });
+ },
setTreeExpanded: (nodeId: string, expanded: boolean) => {
set((state) => {
diff --git a/docs/3.0-guides-and-tools/building-canvas-dashboards.md b/docs/3.0-guides-and-tools/building-canvas-dashboards.md
index 6116cc204d..ce636f200b 100644
--- a/docs/3.0-guides-and-tools/building-canvas-dashboards.md
+++ b/docs/3.0-guides-and-tools/building-canvas-dashboards.md
@@ -180,7 +180,7 @@ Canvas dashboards support multiple ways to create navigation:
Home
Specific Page
Settings
-Account
+Account
```
### Getting Your Drive and Page IDs