mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 20:57:32 +00:00
refactor(sidebar): extract typed app/sidebar architecture and split Sidebar into modular components
- Replace `src/App.jsx` with `src/App.tsx` and move route-level UI orchestration into `src/components/app/AppContent.tsx`. This separates provider/bootstrap concerns from runtime app layout logic, keeps route definitions minimal, and improves readability of the root app entry. - Introduce `src/hooks/useProjectsState.ts` to centralize project/session/sidebar state management previously embedded in `App.jsx`. This keeps the existing behavior for: project loading, Cursor session hydration, WebSocket `loading_progress` handling, additive-update protection for active sessions, URL-based session selection, sidebar refresh/delete/new-session flows. The hook now exposes a typed `sidebarSharedProps` contract and typed handlers used by `AppContent`. - Introduce `src/hooks/useSessionProtection.ts` for active/processing session lifecycle logic. This preserves session-protection behavior while isolating `activeSessions`, `processingSessions`, and temporary-session replacement into a dedicated reusable hook. - Replace monolithic `src/components/Sidebar.jsx` with typed `src/components/Sidebar.tsx` as a thin orchestrator. `Sidebar.tsx` now focuses on wiring controller state/actions, modal visibility, collapsed mode, and version modal behavior instead of rendering every UI branch inline. - Add `src/hooks/useSidebarController.ts` to encapsulate sidebar interaction/state logic. This includes expand/collapse state, inline project/session editing state, project starring/sorting/filtering, lazy session pagination, delete confirmations, rename/delete actions, refresh state, and mobile touch click handling. - Add strongly typed sidebar domain models in `src/components/sidebar/types.ts` and move sidebar-derived helpers into `src/components/sidebar/utils.ts`. Utility coverage now includes: session provider normalization, session view-model creation (name/time/activity/message count), project sorting/filtering, task indicator status derivation, starred-project persistence and readbacks. - Split sidebar rendering into focused components under `src/components/sidebar/`: `SidebarContent.tsx` for top-level sidebar layout composition. `SidebarProjectList.tsx` for project-state branching and project iteration. `SidebarProjectsState.tsx` for loading/empty/no-search-result placeholders. `SidebarProjectItem.tsx` for per-project desktop/mobile header rendering and actions. `SidebarProjectSessions.tsx` for expanded session area, skeletons, pagination, and new-session controls. `SidebarSessionItem.tsx` for per-session desktop/mobile item rendering and session actions. `SessionProviderIcon.tsx` for provider icon normalization. `SidebarHeader.tsx`, `SidebarFooter.tsx`, `SidebarCollapsed.tsx`, and `SidebarModals.tsx` as dedicated typed UI surfaces. This keeps rendering responsibilities local and significantly improves traceability. - Convert shared UI primitives from JSX to TSX: `src/components/ui/button.tsx`, `src/components/ui/input.tsx`, `src/components/ui/badge.tsx`, `src/components/ui/scroll-area.tsx`. These now provide typed props/variants (`forwardRef` where appropriate) while preserving existing class/behavior. - Add shared app typings in `src/types/app.ts` for projects/sessions/websocket/loading contracts used by new hooks/components. - Add global window declarations in `src/types/global.d.ts` for `__ROUTER_BASENAME__`, `refreshProjects`, and `openSettings`, removing implicit `any` usage for global integration points. - Update `src/main.jsx` to import `App.tsx` and keep app bootstrap consistent with the TS migration. - Update `src/components/QuickSettingsPanel.jsx` to self-resolve mobile state via `useDeviceSettings` (remove `isMobile` prop dependency), and update `src/components/ChatInterface.jsx` to render `QuickSettingsPanel` directly. This reduces prop drilling and keeps quick settings colocated with chat UI concerns.
This commit is contained in:
154
src/components/app/AppContent.tsx
Normal file
154
src/components/app/AppContent.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Sidebar from '../Sidebar';
|
||||
import MainContent from '../MainContent';
|
||||
import MobileNav from '../MobileNav';
|
||||
import Settings from '../Settings';
|
||||
|
||||
import { useWebSocket } from '../../contexts/WebSocketContext';
|
||||
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
|
||||
import { useSessionProtection } from '../../hooks/useSessionProtection';
|
||||
import { useProjectsState } from '../../hooks/useProjectsState';
|
||||
|
||||
export default function AppContent() {
|
||||
const navigate = useNavigate();
|
||||
const { sessionId } = useParams<{ sessionId?: string }>();
|
||||
const { t } = useTranslation('common');
|
||||
const { isMobile } = useDeviceSettings({ trackPWA: false });
|
||||
const { ws, sendMessage, latestMessage } = useWebSocket();
|
||||
|
||||
const {
|
||||
activeSessions,
|
||||
processingSessions,
|
||||
markSessionAsActive,
|
||||
markSessionAsInactive,
|
||||
markSessionAsProcessing,
|
||||
markSessionAsNotProcessing,
|
||||
replaceTemporarySession,
|
||||
} = useSessionProtection();
|
||||
|
||||
const {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
sidebarOpen,
|
||||
isLoadingProjects,
|
||||
isInputFocused,
|
||||
showSettings,
|
||||
settingsInitialTab,
|
||||
externalMessageUpdate,
|
||||
setActiveTab,
|
||||
setSidebarOpen,
|
||||
setIsInputFocused,
|
||||
setShowSettings,
|
||||
openSettings,
|
||||
fetchProjects,
|
||||
sidebarSharedProps,
|
||||
} = useProjectsState({
|
||||
sessionId,
|
||||
navigate,
|
||||
latestMessage,
|
||||
isMobile,
|
||||
activeSessions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.refreshProjects = fetchProjects;
|
||||
|
||||
return () => {
|
||||
if (window.refreshProjects === fetchProjects) {
|
||||
delete window.refreshProjects;
|
||||
}
|
||||
};
|
||||
}, [fetchProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
window.openSettings = openSettings;
|
||||
|
||||
return () => {
|
||||
if (window.openSettings === openSettings) {
|
||||
delete window.openSettings;
|
||||
}
|
||||
};
|
||||
}, [openSettings]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex bg-background">
|
||||
{!isMobile ? (
|
||||
<div className="h-full flex-shrink-0 border-r border-border bg-card">
|
||||
<Sidebar {...sidebarSharedProps} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${sidebarOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
onTouchStart={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
aria-label={t('versionUpdate.ariaLabels.closeSidebar')}
|
||||
/>
|
||||
<div
|
||||
className={`relative w-[85vw] max-w-sm sm:w-80 h-full bg-card border-r border-border transform transition-transform duration-150 ease-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onTouchStart={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Sidebar {...sidebarSharedProps} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-mobile-nav' : ''}`}>
|
||||
<MainContent
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
ws={ws}
|
||||
sendMessage={sendMessage}
|
||||
latestMessage={latestMessage}
|
||||
isMobile={isMobile}
|
||||
onMenuClick={() => setSidebarOpen(true)}
|
||||
isLoading={isLoadingProjects}
|
||||
onInputFocusChange={setIsInputFocused}
|
||||
onSessionActive={markSessionAsActive}
|
||||
onSessionInactive={markSessionAsInactive}
|
||||
onSessionProcessing={markSessionAsProcessing}
|
||||
onSessionNotProcessing={markSessionAsNotProcessing}
|
||||
processingSessions={processingSessions}
|
||||
onReplaceTemporarySession={replaceTemporarySession}
|
||||
onNavigateToSession={(targetSessionId: string) => navigate(`/session/${targetSessionId}`)}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
externalMessageUpdate={externalMessageUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<MobileNav
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
isInputFocused={isInputFocused}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Settings
|
||||
isOpen={showSettings}
|
||||
onClose={() => setShowSettings(false)}
|
||||
projects={projects as any}
|
||||
initialTab={settingsInitialTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user