refactor(main-content): migrate MainContent to TypeScript and modularize UI/state boundaries

Replace the previous monolithic MainContent.jsx with a typed one and
extract focused subcomponents/hooks to improve readability, local state ownership,
and maintainability while keeping runtime behavior unchanged.

Key changes:
- Replace `src/components/MainContent.jsx` with `src/components/MainContent.tsx`.
- Add typed contracts for main-content domain in `src/components/main-content/types.ts`.
- Extract header composition into:
  - `MainContentHeader.tsx`
  - `MainContentTitle.tsx`
  - `MainContentTabSwitcher.tsx`
  - `MobileMenuButton.tsx`
- Extract loading/empty project views into `MainContentStateView.tsx`.
- Extract editor presentation into `EditorSidebar.tsx`.
- Move editor file-open + resize behavior into `useEditorSidebar.ts`.
- Move mobile menu touch/click suppression logic into `useMobileMenuHandlers.ts`.
- Extract TaskMaster-specific concerns into `TaskMasterPanel.tsx`:
  - task detail modal state
  - PRD editor modal state
  - PRD list loading/refresh
  - PRD save notification lifecycle

Behavior/compatibility notes:
- Preserve existing tab behavior, session passthrough props, and Chat/Git/File flows.
- Keep interop with existing JS components via boundary `as any` casts where needed.
- No intentional functional changes; this commit is structural/type-oriented refactor.

Validation:
- `npm run typecheck` passes.
- `npm run build` passes (existing unrelated CSS minify warnings remain).
This commit is contained in:
Haileyesus
2026-02-07 17:23:56 +03:00
parent 8608d32dbd
commit cdc03e754f
12 changed files with 999 additions and 713 deletions

View 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 '../../components/main-content/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,
};
}

View 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,
};
}