mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
refactor: setup tab switchers with route changers
This commit is contained in:
168
src/App.tsx
168
src/App.tsx
@@ -1,55 +1,153 @@
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import {
|
||||
Navigate,
|
||||
Outlet,
|
||||
RouterProvider,
|
||||
createBrowserRouter,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { AuthProvider, ProtectedRoute } from './components/auth';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { PluginsProvider } from './contexts/PluginsContext';
|
||||
import { TaskMasterProvider } from './contexts/TaskMasterContext';
|
||||
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
||||
import { WebSocketProvider } from './contexts/WebSocketContext';
|
||||
import i18n from './i18n/config.js';
|
||||
import { RootLayout } from '@/components/refactored/shared/RootLayout';
|
||||
import { SystemUIProvider } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIProvider';
|
||||
import { RootLayout } from '@/components/refactored/shared/layout/RootLayout';
|
||||
|
||||
// Mock page components
|
||||
const Home = () => <div className="p-8"><h1>Home Page</h1><p>Select a session or create a new project.</p></div>;
|
||||
const WorkspaceContent = () => <div className="p-8"><h1>Workspace View</h1><p>Select a session or start a new one.</p></div>;
|
||||
const SessionContent = () => <div className="p-8"><h1>Session View</h1><p>Chat interface goes here.</p></div>;
|
||||
const isValidRouteTab = (value: string | undefined): boolean => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <RootLayout />, // The layout wraps all children
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: "/session/:sessionId",
|
||||
element: <SessionContent />,
|
||||
},
|
||||
{
|
||||
path: "/sessions/:sessionId",
|
||||
element: <SessionContent />,
|
||||
},
|
||||
{
|
||||
path: "/workspace/:workspaceId",
|
||||
element: <WorkspaceContent />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const normalizedValue = decodeURIComponent(value);
|
||||
return (
|
||||
normalizedValue === 'chat' ||
|
||||
normalizedValue === 'shell' ||
|
||||
normalizedValue === 'files' ||
|
||||
normalizedValue === 'git' ||
|
||||
normalizedValue === 'tasks' ||
|
||||
normalizedValue === 'preview' ||
|
||||
normalizedValue.startsWith('plugin:')
|
||||
);
|
||||
};
|
||||
|
||||
function NoWorkspaceRoute() {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8">
|
||||
<div className="max-w-md space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold">Choose Your Project</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is the root route (`/`) empty state. Select a workspace from the sidebar to continue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceLayout() {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
if (!workspaceId) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceTabRoute() {
|
||||
const { workspaceId, sessionId, tab } = useParams<{
|
||||
workspaceId: string;
|
||||
sessionId?: string;
|
||||
tab: string;
|
||||
}>();
|
||||
|
||||
if (!workspaceId) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
if (!isValidRouteTab(tab)) {
|
||||
return <Navigate to="../chat" replace />;
|
||||
}
|
||||
|
||||
const decodedWorkspaceId = decodeURIComponent(workspaceId);
|
||||
const decodedSessionId = sessionId ? decodeURIComponent(sessionId) : null;
|
||||
const decodedTab = tab ? decodeURIComponent(tab) : 'chat';
|
||||
|
||||
return (
|
||||
<div className="h-full p-6">
|
||||
<div className="rounded-xl border border-border/70 bg-card/30 p-5">
|
||||
<h2 className="text-lg font-semibold">{decodedTab} view</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Workspace: <span className="font-medium text-foreground">{decodedWorkspaceId}</span>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Session:{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{decodedSessionId || 'none (workspace-level tab)'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: '/',
|
||||
element: <RootLayout />,
|
||||
children: [
|
||||
{ index: true, element: <NoWorkspaceRoute /> },
|
||||
{
|
||||
path: 'workspaces/:workspaceId',
|
||||
element: <WorkspaceLayout />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="chat" replace /> },
|
||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
||||
{
|
||||
path: 'sessions/:sessionId',
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="chat" replace /> },
|
||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{ basename: window.__ROUTER_BASENAME__ || '' },
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ProtectedRoute>
|
||||
<RouterProvider router={router} />
|
||||
</ProtectedRoute>
|
||||
<WebSocketProvider>
|
||||
<PluginsProvider>
|
||||
<TasksSettingsProvider>
|
||||
<TaskMasterProvider>
|
||||
<SystemUIProvider>
|
||||
<ProtectedRoute>
|
||||
<RouterProvider router={router} />
|
||||
</ProtectedRoute>
|
||||
</SystemUIProvider>
|
||||
</TaskMasterProvider>
|
||||
</TasksSettingsProvider>
|
||||
</PluginsProvider>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
// import { I18nextProvider } from 'react-i18next';
|
||||
// import { ThemeProvider } from './contexts/ThemeContext';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createContext } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export type SystemUIContextValue = {
|
||||
sidebarIsCollapsed: boolean;
|
||||
setSidebarIsCollapsed: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const SystemUIContext = createContext<SystemUIContextValue | null>(null);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMemo, useState, type ReactNode } from 'react';
|
||||
import { SystemUIContext, type SystemUIContextValue } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIContext';
|
||||
|
||||
export function SystemUIProvider({ children }: { children: ReactNode }) {
|
||||
const [sidebarIsCollapsed, setSidebarIsCollapsed] = useState(false);
|
||||
|
||||
const value = useMemo<SystemUIContextValue>(
|
||||
() => ({
|
||||
sidebarIsCollapsed,
|
||||
setSidebarIsCollapsed,
|
||||
}),
|
||||
[sidebarIsCollapsed],
|
||||
);
|
||||
|
||||
return (
|
||||
<SystemUIContext.Provider value={value}>
|
||||
{children}
|
||||
</SystemUIContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { SystemUIContext } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIContext';
|
||||
|
||||
export const useSystemUI = () => {
|
||||
const context = useContext(SystemUIContext);
|
||||
if (!context) {
|
||||
throw new Error('useSystemUI must be used within SystemUIProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
187
src/components/refactored/shared/layout/MainHeading.tsx
Normal file
187
src/components/refactored/shared/layout/MainHeading.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AppTab } from '@/types/app';
|
||||
import { usePlugins } from '@/contexts/PluginsContext';
|
||||
import { useDeviceSettings } from '@/hooks/useDeviceSettings';
|
||||
import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI';
|
||||
import { MainHeadingTabSwitcher } from '@/components/refactored/shared/layout/MainHeadingTabSwitcher';
|
||||
|
||||
type MainHeadingRouteParams = {
|
||||
workspaceId?: string;
|
||||
sessionId?: string;
|
||||
tab?: string;
|
||||
};
|
||||
|
||||
const decodeValue = (value?: string): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabTitle = (tab: AppTab, pluginDisplayName: string | undefined, t: (key: string) => string) => {
|
||||
if (tab.startsWith('plugin:') && pluginDisplayName) {
|
||||
return pluginDisplayName;
|
||||
}
|
||||
|
||||
if (tab === 'files') {
|
||||
return t('mainContent.projectFiles');
|
||||
}
|
||||
|
||||
if (tab === 'git') {
|
||||
return t('tabs.git');
|
||||
}
|
||||
|
||||
if (tab === 'tasks') {
|
||||
return 'TaskMaster';
|
||||
}
|
||||
|
||||
if (tab === 'shell') {
|
||||
return t('tabs.shell');
|
||||
}
|
||||
|
||||
return t('tabs.chat');
|
||||
};
|
||||
|
||||
export function MainHeading() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(['common', 'sidebar']);
|
||||
const { plugins } = usePlugins();
|
||||
const { isMobile } = useDeviceSettings({ trackPWA: false });
|
||||
const { sidebarIsCollapsed, setSidebarIsCollapsed } = useSystemUI();
|
||||
const { workspaceId, sessionId, tab } = useParams<MainHeadingRouteParams>();
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const decodedWorkspaceId = useMemo(() => decodeValue(workspaceId), [workspaceId]);
|
||||
const decodedSessionId = useMemo(() => decodeValue(sessionId), [sessionId]);
|
||||
const activeTab = useMemo<AppTab>(() => {
|
||||
const routeTab = decodeValue(tab);
|
||||
return (routeTab || 'chat') as AppTab;
|
||||
}, [tab]);
|
||||
|
||||
const pluginDisplayName = useMemo(
|
||||
() =>
|
||||
activeTab.startsWith('plugin:')
|
||||
? plugins.find((plugin) => plugin.name === activeTab.replace('plugin:', ''))?.displayName
|
||||
: undefined,
|
||||
[activeTab, plugins],
|
||||
);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (activeTab === 'chat' && decodedSessionId) {
|
||||
return decodedSessionId;
|
||||
}
|
||||
|
||||
if (activeTab === 'chat') {
|
||||
return t('mainContent.newSession');
|
||||
}
|
||||
|
||||
return getTabTitle(activeTab, pluginDisplayName, t);
|
||||
}, [activeTab, decodedSessionId, pluginDisplayName, t]);
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const element = scrollRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollLeft(element.scrollLeft > 2);
|
||||
setCanScrollRight(element.scrollLeft < element.scrollWidth - element.clientWidth - 2);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollRef.current;
|
||||
if (!element || isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateScrollState();
|
||||
const observer = new ResizeObserver(updateScrollState);
|
||||
observer.observe(element);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [isMobile, updateScrollState]);
|
||||
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleTabSelect = (nextTab: AppTab) => {
|
||||
// Preserve workspace/session context while switching only the active tab path segment.
|
||||
const encodedWorkspaceId = encodeURIComponent(decodedWorkspaceId);
|
||||
const encodedTab = encodeURIComponent(nextTab);
|
||||
|
||||
if (decodedSessionId) {
|
||||
navigate(
|
||||
`/workspaces/${encodedWorkspaceId}/sessions/${encodeURIComponent(decodedSessionId)}/${encodedTab}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/workspaces/${encodedWorkspaceId}/${encodedTab}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pwa-header-safe flex-shrink-0 border-b border-border/60 bg-background px-3 py-1.5 sm:px-4 sm:py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={() => setSidebarIsCollapsed((previousValue) => !previousValue)}
|
||||
className="pwa-menu-button flex-shrink-0 touch-manipulation rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground active:scale-95"
|
||||
aria-label={sidebarIsCollapsed ? t('common:versionUpdate.ariaLabels.showSidebar') : t('sidebar:tooltips.hideSidebar')}
|
||||
title={sidebarIsCollapsed ? t('common:versionUpdate.ariaLabels.showSidebar') : t('sidebar:tooltips.hideSidebar')}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="scrollbar-hide flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="scrollbar-hide overflow-x-auto whitespace-nowrap text-sm font-semibold leading-tight text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="truncate text-[11px] leading-tight text-muted-foreground">
|
||||
{decodedWorkspaceId}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<div className="relative min-w-0 flex-shrink overflow-hidden sm:flex-shrink-0">
|
||||
{canScrollLeft && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-background to-transparent" />
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={updateScrollState}
|
||||
className="scrollbar-hide overflow-x-auto"
|
||||
>
|
||||
<MainHeadingTabSwitcher activeTab={activeTab} onTabSelect={handleTabSelect} />
|
||||
</div>
|
||||
{canScrollRight && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-background to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ClipboardCheck, Folder, GitBranch, MessageSquare, Terminal, type LucideIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlugins } from '@/contexts/PluginsContext';
|
||||
import { useTasksSettings } from '@/contexts/TasksSettingsContext';
|
||||
import { Pill, PillBar, Tooltip } from '@/shared/view/ui';
|
||||
import type { AppTab } from '@/types/app';
|
||||
import PluginIcon from '@/components/plugins/view/PluginIcon';
|
||||
|
||||
type TasksSettingsContextValue = {
|
||||
tasksEnabled: boolean;
|
||||
isTaskMasterInstalled: boolean | null;
|
||||
};
|
||||
|
||||
type BuiltInTab = {
|
||||
kind: 'builtin';
|
||||
id: AppTab;
|
||||
labelKey: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
type PluginTab = {
|
||||
kind: 'plugin';
|
||||
id: AppTab;
|
||||
label: string;
|
||||
pluginName: string;
|
||||
iconFile: string;
|
||||
};
|
||||
|
||||
type TabDefinition = BuiltInTab | PluginTab;
|
||||
|
||||
type MainHeadingTabSwitcherProps = {
|
||||
activeTab: AppTab;
|
||||
onTabSelect: (tabId: AppTab) => void;
|
||||
};
|
||||
|
||||
const BASE_TABS: BuiltInTab[] = [
|
||||
{ kind: 'builtin', id: 'chat', labelKey: 'tabs.chat', icon: MessageSquare },
|
||||
{ kind: 'builtin', id: 'shell', labelKey: 'tabs.shell', icon: Terminal },
|
||||
{ kind: 'builtin', id: 'files', labelKey: 'tabs.files', icon: Folder },
|
||||
{ kind: 'builtin', id: 'git', labelKey: 'tabs.git', icon: GitBranch },
|
||||
];
|
||||
|
||||
const TASKS_TAB: BuiltInTab = {
|
||||
kind: 'builtin',
|
||||
id: 'tasks',
|
||||
labelKey: 'tabs.tasks',
|
||||
icon: ClipboardCheck,
|
||||
};
|
||||
|
||||
export function MainHeadingTabSwitcher({ activeTab, onTabSelect }: MainHeadingTabSwitcherProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const { plugins } = usePlugins();
|
||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue;
|
||||
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
||||
|
||||
const builtInTabs: BuiltInTab[] = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
|
||||
const pluginTabs: PluginTab[] = plugins
|
||||
.filter((plugin) => plugin.enabled)
|
||||
.map((plugin) => ({
|
||||
kind: 'plugin',
|
||||
id: `plugin:${plugin.name}` as AppTab,
|
||||
label: plugin.displayName,
|
||||
pluginName: plugin.name,
|
||||
iconFile: plugin.icon,
|
||||
}));
|
||||
|
||||
const tabs: TabDefinition[] = [...builtInTabs, ...pluginTabs];
|
||||
|
||||
return (
|
||||
<PillBar>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
const displayLabel = tab.kind === 'builtin' ? t(tab.labelKey) : tab.label;
|
||||
|
||||
return (
|
||||
<Tooltip key={tab.id} content={displayLabel} position="bottom">
|
||||
<Pill
|
||||
isActive={isActive}
|
||||
onClick={() => onTabSelect(tab.id)}
|
||||
className="px-2.5 py-[5px]"
|
||||
>
|
||||
{tab.kind === 'builtin' ? (
|
||||
<tab.icon className="h-3.5 w-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||
) : (
|
||||
<PluginIcon
|
||||
pluginName={tab.pluginName}
|
||||
iconFile={tab.iconFile}
|
||||
className="flex h-3.5 w-3.5 items-center justify-center [&>svg]:h-full [&>svg]:w-full"
|
||||
/>
|
||||
)}
|
||||
<span className="hidden lg:inline">{displayLabel}</span>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</PillBar>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from '@/components/refactored/sidebar/view/Sidebar';
|
||||
|
||||
import { MainHeading } from '@/components/refactored/shared/layout/MainHeading';
|
||||
|
||||
export function RootLayout() {
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background">
|
||||
<Sidebar />
|
||||
<main className="relative flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<MainHeading />
|
||||
<main className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Hook layer (The Manager)
|
||||
* Manages the layout states for the sidebar, such as collapse/open.
|
||||
*/
|
||||
export const useSidebarSettings = () => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const toggleCollapse = () => setIsCollapsed((prev) => !prev);
|
||||
const setCollapsed = (value: boolean) => setIsCollapsed(value);
|
||||
|
||||
return {
|
||||
isCollapsed,
|
||||
toggleCollapse,
|
||||
setCollapsed,
|
||||
};
|
||||
};
|
||||
@@ -1,18 +1,23 @@
|
||||
import SidebarFooter from './SidebarFooter';
|
||||
import { useSidebarSettings } from '@/components/refactored/sidebar/hooks/useSidebarSettings';
|
||||
import { useSidebarModals } from '@/components/refactored/sidebar/hooks/useSidebarModals';
|
||||
import { useWorkspaces } from '@/components/refactored/sidebar/hooks/useWorkspaces';
|
||||
import SidebarHeader from '@/components/refactored/sidebar/view/SidebarHeader';
|
||||
import { SidebarDeleteModals } from '@/components/refactored/sidebar/view/SidebarDeleteModals';
|
||||
import { SidebarWorkspaceList } from '@/components/refactored/sidebar/view/SidebarWorkspaceList';
|
||||
import { SidebarCollapsed } from '@/components/refactored/sidebar/view/SidebarCollapsed';
|
||||
import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI';
|
||||
import { useDeviceSettings } from '@/hooks/useDeviceSettings';
|
||||
import { cn } from '@/lib/utils';
|
||||
import ProjectCreationWizard from '@/components/project-creation-wizard';
|
||||
import VersionUpgradeModal from '@/components/version-upgrade/view';
|
||||
import Settings from '@/components/settings/view/Settings';
|
||||
|
||||
export function Sidebar() {
|
||||
const { isCollapsed, toggleCollapse, setCollapsed } = useSidebarSettings();
|
||||
const { isMobile } = useDeviceSettings({ trackPWA: false });
|
||||
const { sidebarIsCollapsed, setSidebarIsCollapsed } = useSystemUI();
|
||||
const isCollapsed = sidebarIsCollapsed;
|
||||
const toggleCollapse = () => setSidebarIsCollapsed((previousValue) => !previousValue);
|
||||
const setCollapsed = (value: boolean) => setSidebarIsCollapsed(value);
|
||||
const {
|
||||
workspaces,
|
||||
starredWorkspaces,
|
||||
@@ -148,7 +153,8 @@ export function Sidebar() {
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{isCollapsed && (
|
||||
{/* Keep collapsed rail desktop-only; mobile uses the header hamburger to reopen. */}
|
||||
{isCollapsed && !isMobile && (
|
||||
<aside className="fixed inset-y-0 left-0 z-40 h-full border-r border-border md:relative">
|
||||
<SidebarCollapsed
|
||||
onExpand={() => setCollapsed(false)}
|
||||
|
||||
Reference in New Issue
Block a user