mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-16 13:47:34 +00:00
refactor: Restructure files and folders to better mimic feature-based architecture
This commit is contained in:
110
src/components/main-content/hooks/useEditorSidebar.ts
Normal file
110
src/components/main-content/hooks/useEditorSidebar.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { Project } from '../../../types/app';
|
||||
import type { DiffInfo, EditingFile } from '../types/types';
|
||||
|
||||
type UseEditorSidebarOptions = {
|
||||
selectedProject: Project | null;
|
||||
isMobile: boolean;
|
||||
initialWidth?: number;
|
||||
};
|
||||
|
||||
export function useEditorSidebar({
|
||||
selectedProject,
|
||||
isMobile,
|
||||
initialWidth = 600,
|
||||
}: UseEditorSidebarOptions) {
|
||||
const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
|
||||
const [editorWidth, setEditorWidth] = useState(initialWidth);
|
||||
const [editorExpanded, setEditorExpanded] = useState(false);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const resizeHandleRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleFileOpen = useCallback(
|
||||
(filePath: string, diffInfo: DiffInfo | null = null) => {
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const fileName = normalizedPath.split('/').pop() || filePath;
|
||||
|
||||
setEditingFile({
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
projectName: selectedProject?.name,
|
||||
diffInfo,
|
||||
});
|
||||
},
|
||||
[selectedProject?.name],
|
||||
);
|
||||
|
||||
const handleCloseEditor = useCallback(() => {
|
||||
setEditingFile(null);
|
||||
setEditorExpanded(false);
|
||||
}, []);
|
||||
|
||||
const handleToggleEditorExpand = useCallback(() => {
|
||||
setEditorExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResizing(true);
|
||||
event.preventDefault();
|
||||
},
|
||||
[isMobile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: globalThis.MouseEvent) => {
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = resizeHandleRef.current?.parentElement;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const newWidth = containerRect.right - event.clientX;
|
||||
|
||||
const minWidth = 300;
|
||||
const maxWidth = containerRect.width * 0.8;
|
||||
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
setEditorWidth(newWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
if (isResizing) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
return {
|
||||
editingFile,
|
||||
editorWidth,
|
||||
editorExpanded,
|
||||
resizeHandleRef,
|
||||
handleFileOpen,
|
||||
handleCloseEditor,
|
||||
handleToggleEditorExpand,
|
||||
handleResizeStart,
|
||||
};
|
||||
}
|
||||
50
src/components/main-content/hooks/useMobileMenuHandlers.ts
Normal file
50
src/components/main-content/hooks/useMobileMenuHandlers.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import type { MouseEvent, TouchEvent } from 'react';
|
||||
|
||||
type MenuEvent = MouseEvent<HTMLButtonElement> | TouchEvent<HTMLButtonElement>;
|
||||
|
||||
export function useMobileMenuHandlers(onMenuClick: () => void) {
|
||||
const suppressNextMenuClickRef = useRef(false);
|
||||
|
||||
const openMobileMenu = useCallback(
|
||||
(event?: MenuEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onMenuClick();
|
||||
},
|
||||
[onMenuClick],
|
||||
);
|
||||
|
||||
const handleMobileMenuTouchEnd = useCallback(
|
||||
(event: TouchEvent<HTMLButtonElement>) => {
|
||||
suppressNextMenuClickRef.current = true;
|
||||
openMobileMenu(event);
|
||||
|
||||
window.setTimeout(() => {
|
||||
suppressNextMenuClickRef.current = false;
|
||||
}, 350);
|
||||
},
|
||||
[openMobileMenu],
|
||||
);
|
||||
|
||||
const handleMobileMenuClick = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (suppressNextMenuClickRef.current) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
openMobileMenu(event);
|
||||
},
|
||||
[openMobileMenu],
|
||||
);
|
||||
|
||||
return {
|
||||
handleMobileMenuClick,
|
||||
handleMobileMenuTouchEnd,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Dispatch, MouseEvent, RefObject, SetStateAction } from 'react';
|
||||
import type { AppTab, Project, ProjectSession } from '../../types/app';
|
||||
import type { AppTab, Project, ProjectSession } from '../../../types/app';
|
||||
|
||||
export type SessionLifecycleHandler = (sessionId?: string | null) => void;
|
||||
|
||||
181
src/components/main-content/view/MainContent.tsx
Normal file
181
src/components/main-content/view/MainContent.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import ChatInterface from '../../chat/view/ChatInterface';
|
||||
import FileTree from '../../FileTree';
|
||||
import StandaloneShell from '../../StandaloneShell';
|
||||
import GitPanel from '../../GitPanel';
|
||||
import ErrorBoundary from '../../ErrorBoundary';
|
||||
|
||||
import MainContentHeader from './subcomponents/MainContentHeader';
|
||||
import MainContentStateView from './subcomponents/MainContentStateView';
|
||||
import EditorSidebar from './subcomponents/EditorSidebar';
|
||||
import TaskMasterPanel from './subcomponents/TaskMasterPanel';
|
||||
import type { MainContentProps } from '../types/types';
|
||||
|
||||
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import { useUiPreferences } from '../../../hooks/useUiPreferences';
|
||||
import { useEditorSidebar } from '../hooks/useEditorSidebar';
|
||||
import type { Project } from '../../../types/app';
|
||||
|
||||
const AnyStandaloneShell = StandaloneShell as any;
|
||||
const AnyGitPanel = GitPanel as any;
|
||||
|
||||
type TaskMasterContextValue = {
|
||||
currentProject?: Project | null;
|
||||
setCurrentProject?: ((project: Project) => void) | null;
|
||||
};
|
||||
|
||||
type TasksSettingsContextValue = {
|
||||
tasksEnabled: boolean;
|
||||
isTaskMasterInstalled: boolean | null;
|
||||
isTaskMasterReady: boolean | null;
|
||||
};
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
ws,
|
||||
sendMessage,
|
||||
latestMessage,
|
||||
isMobile,
|
||||
onMenuClick,
|
||||
isLoading,
|
||||
onInputFocusChange,
|
||||
onSessionActive,
|
||||
onSessionInactive,
|
||||
onSessionProcessing,
|
||||
onSessionNotProcessing,
|
||||
processingSessions,
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
onShowSettings,
|
||||
externalMessageUpdate,
|
||||
}: MainContentProps) {
|
||||
const { preferences } = useUiPreferences();
|
||||
const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
|
||||
|
||||
const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue;
|
||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue;
|
||||
|
||||
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
||||
|
||||
const {
|
||||
editingFile,
|
||||
editorWidth,
|
||||
editorExpanded,
|
||||
resizeHandleRef,
|
||||
handleFileOpen,
|
||||
handleCloseEditor,
|
||||
handleToggleEditorExpand,
|
||||
handleResizeStart,
|
||||
} = useEditorSidebar({
|
||||
selectedProject,
|
||||
isMobile,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject && selectedProject !== currentProject) {
|
||||
setCurrentProject?.(selectedProject);
|
||||
}
|
||||
}, [selectedProject, currentProject, setCurrentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShowTasksTab && activeTab === 'tasks') {
|
||||
setActiveTab('chat');
|
||||
}
|
||||
}, [shouldShowTasksTab, activeTab, setActiveTab]);
|
||||
|
||||
if (isLoading) {
|
||||
return <MainContentStateView mode="loading" isMobile={isMobile} onMenuClick={onMenuClick} />;
|
||||
}
|
||||
|
||||
if (!selectedProject) {
|
||||
return <MainContentStateView mode="empty" isMobile={isMobile} onMenuClick={onMenuClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<MainContentHeader
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
shouldShowTasksTab={shouldShowTasksTab}
|
||||
isMobile={isMobile}
|
||||
onMenuClick={onMenuClick}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex min-h-0 overflow-hidden">
|
||||
<div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editorExpanded ? 'hidden' : ''}`}>
|
||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||
<ErrorBoundary showDetails>
|
||||
<ChatInterface
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
ws={ws}
|
||||
sendMessage={sendMessage}
|
||||
latestMessage={latestMessage}
|
||||
onFileOpen={handleFileOpen}
|
||||
onInputFocusChange={onInputFocusChange}
|
||||
onSessionActive={onSessionActive}
|
||||
onSessionInactive={onSessionInactive}
|
||||
onSessionProcessing={onSessionProcessing}
|
||||
onSessionNotProcessing={onSessionNotProcessing}
|
||||
processingSessions={processingSessions}
|
||||
onReplaceTemporarySession={onReplaceTemporarySession}
|
||||
onNavigateToSession={onNavigateToSession}
|
||||
onShowSettings={onShowSettings}
|
||||
autoExpandTools={autoExpandTools}
|
||||
showRawParameters={showRawParameters}
|
||||
showThinking={showThinking}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
externalMessageUpdate={externalMessageUpdate}
|
||||
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{activeTab === 'files' && (
|
||||
<div className="h-full overflow-hidden">
|
||||
<FileTree selectedProject={selectedProject} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'shell' && (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<AnyStandaloneShell project={selectedProject} session={selectedSession} showHeader={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'git' && (
|
||||
<div className="h-full overflow-hidden">
|
||||
<AnyGitPanel selectedProject={selectedProject} isMobile={isMobile} onFileOpen={handleFileOpen} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowTasksTab && <TaskMasterPanel isVisible={activeTab === 'tasks'} />}
|
||||
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'preview' ? 'block' : 'hidden'}`} />
|
||||
</div>
|
||||
|
||||
<EditorSidebar
|
||||
editingFile={editingFile}
|
||||
isMobile={isMobile}
|
||||
editorExpanded={editorExpanded}
|
||||
editorWidth={editorWidth}
|
||||
resizeHandleRef={resizeHandleRef}
|
||||
onResizeStart={handleResizeStart}
|
||||
onCloseEditor={handleCloseEditor}
|
||||
onToggleEditorExpand={handleToggleEditorExpand}
|
||||
projectPath={selectedProject.path}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(MainContent);
|
||||
@@ -1,5 +1,5 @@
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import type { EditorSidebarProps } from './types';
|
||||
import CodeEditor from '../../../CodeEditor';
|
||||
import type { EditorSidebarProps } from '../../types/types';
|
||||
|
||||
const AnyCodeEditor = CodeEditor as any;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MobileMenuButton from './MobileMenuButton';
|
||||
import MainContentTabSwitcher from './MainContentTabSwitcher';
|
||||
import MainContentTitle from './MainContentTitle';
|
||||
import type { MainContentHeaderProps } from './types';
|
||||
import type { MainContentHeaderProps } from '../../types/types';
|
||||
|
||||
export default function MainContentHeader({
|
||||
activeTab,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MobileMenuButton from './MobileMenuButton';
|
||||
import type { MainContentStateViewProps } from './types';
|
||||
import type { MainContentStateViewProps } from '../../types/types';
|
||||
|
||||
export default function MainContentStateView({ mode, isMobile, onMenuClick }: MainContentStateViewProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,5 +1,5 @@
|
||||
import Tooltip from '../Tooltip';
|
||||
import type { AppTab } from '../../types/app';
|
||||
import Tooltip from '../../../Tooltip';
|
||||
import type { AppTab } from '../../../../types/app';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SessionProviderLogo from '../SessionProviderLogo';
|
||||
import type { AppTab, Project, ProjectSession } from '../../types/app';
|
||||
import SessionProviderLogo from '../../../SessionProviderLogo';
|
||||
import type { AppTab, Project, ProjectSession } from '../../../../types/app';
|
||||
|
||||
type MainContentTitleProps = {
|
||||
activeTab: AppTab;
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MobileMenuButtonProps } from './types';
|
||||
import { useMobileMenuHandlers } from '../../hooks/main-content/useMobileMenuHandlers';
|
||||
import type { MobileMenuButtonProps } from '../../types/types';
|
||||
import { useMobileMenuHandlers } from '../../hooks/useMobileMenuHandlers';
|
||||
|
||||
export default function MobileMenuButton({ onMenuClick, compact = false }: MobileMenuButtonProps) {
|
||||
const { handleMobileMenuClick, handleMobileMenuTouchEnd } = useMobileMenuHandlers(onMenuClick);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import TaskList from '../TaskList';
|
||||
import TaskDetail from '../TaskDetail';
|
||||
import PRDEditor from '../PRDEditor';
|
||||
import { useTaskMaster } from '../../contexts/TaskMasterContext';
|
||||
import { api } from '../../utils/api';
|
||||
import type { Project } from '../../types/app';
|
||||
import type { PrdFile, TaskMasterPanelProps, TaskMasterTask, TaskSelection } from './types';
|
||||
import TaskList from '../../../TaskList';
|
||||
import TaskDetail from '../../../TaskDetail';
|
||||
import PRDEditor from '../../../PRDEditor';
|
||||
import { useTaskMaster } from '../../../../contexts/TaskMasterContext';
|
||||
import { api } from '../../../../utils/api';
|
||||
import type { Project } from '../../../../types/app';
|
||||
import type { PrdFile, TaskMasterPanelProps, TaskMasterTask, TaskSelection } from '../../types/types';
|
||||
|
||||
const AnyTaskList = TaskList as any;
|
||||
const AnyTaskDetail = TaskDetail as any;
|
||||
Reference in New Issue
Block a user