mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
♻️ refactor: extract Phase 1 hooks from sidebar (2187→2099 lines)
Extract 3 simple hooks with no UI dependencies: - use-theme-preview.ts: Debounced theme preview on hover - use-sidebar-auto-collapse.ts: Auto-collapse on small screens - use-drag-and-drop.ts: Project reordering drag-and-drop Benefits: - Reduced sidebar.tsx by 88 lines (-4%) - Improved testability (hooks can be tested in isolation) - Removed unused imports (DragEndEvent, PointerSensor, useSensor, useSensors) - Created hooks/ barrel export pattern Next steps: Extract 10+ remaining hooks and 10+ UI sections to reach target of 200-300 lines (current: 2099 lines, need to reduce ~1800 more) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,14 +75,7 @@ import { DeleteProjectDialog } from '@/components/views/settings-view/components
|
|||||||
import { NewProjectModal } from '@/components/dialogs/new-project-modal';
|
import { NewProjectModal } from '@/components/dialogs/new-project-modal';
|
||||||
import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
|
import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
|
||||||
import type { FeatureCount } from '@/components/views/spec-view/types';
|
import type { FeatureCount } from '@/components/views/spec-view/types';
|
||||||
import {
|
import { DndContext, closestCenter } from '@dnd-kit/core';
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
closestCenter,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
import type { StarterTemplate } from '@/lib/templates';
|
import type { StarterTemplate } from '@/lib/templates';
|
||||||
@@ -95,6 +88,7 @@ import {
|
|||||||
PROJECT_LIGHT_THEMES,
|
PROJECT_LIGHT_THEMES,
|
||||||
SIDEBAR_FEATURE_FLAGS,
|
SIDEBAR_FEATURE_FLAGS,
|
||||||
} from './sidebar/constants';
|
} from './sidebar/constants';
|
||||||
|
import { useThemePreview, useSidebarAutoCollapse, useDragAndDrop } from './sidebar/hooks';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -164,45 +158,8 @@ export function Sidebar() {
|
|||||||
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
||||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||||
|
|
||||||
// Debounced preview theme handlers to prevent excessive re-renders
|
// Debounced preview theme handlers
|
||||||
const previewTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme });
|
||||||
|
|
||||||
const handlePreviewEnter = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
// Clear any pending timeout
|
|
||||||
if (previewTimeoutRef.current) {
|
|
||||||
clearTimeout(previewTimeoutRef.current);
|
|
||||||
}
|
|
||||||
// Small delay to debounce rapid hover changes
|
|
||||||
previewTimeoutRef.current = setTimeout(() => {
|
|
||||||
setPreviewTheme(value as ThemeMode);
|
|
||||||
}, 16); // ~1 frame delay
|
|
||||||
},
|
|
||||||
[setPreviewTheme]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePreviewLeave = useCallback(
|
|
||||||
(e: React.PointerEvent) => {
|
|
||||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
|
||||||
if (!relatedTarget?.closest('[data-testid^="project-theme-"]')) {
|
|
||||||
// Clear any pending timeout
|
|
||||||
if (previewTimeoutRef.current) {
|
|
||||||
clearTimeout(previewTimeoutRef.current);
|
|
||||||
}
|
|
||||||
setPreviewTheme(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setPreviewTheme]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup timeout on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (previewTimeoutRef.current) {
|
|
||||||
clearTimeout(previewTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Derive isCreatingSpec from store state
|
// Derive isCreatingSpec from store state
|
||||||
const isCreatingSpec = specCreatingForProject !== null;
|
const isCreatingSpec = specCreatingForProject !== null;
|
||||||
@@ -212,23 +169,7 @@ export function Sidebar() {
|
|||||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Auto-collapse sidebar on small screens
|
// Auto-collapse sidebar on small screens
|
||||||
useEffect(() => {
|
useSidebarAutoCollapse({ sidebarOpen, toggleSidebar });
|
||||||
const mediaQuery = window.matchMedia('(max-width: 1024px)'); // lg breakpoint
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
if (mediaQuery.matches && sidebarOpen) {
|
|
||||||
// Auto-collapse on small screens
|
|
||||||
toggleSidebar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check on mount
|
|
||||||
handleResize();
|
|
||||||
|
|
||||||
// Listen for changes
|
|
||||||
mediaQuery.addEventListener('change', handleResize);
|
|
||||||
return () => mediaQuery.removeEventListener('change', handleResize);
|
|
||||||
}, [sidebarOpen, toggleSidebar]);
|
|
||||||
|
|
||||||
// Filtered projects based on search query
|
// Filtered projects based on search query
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
@@ -262,31 +203,8 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [isProjectPickerOpen]);
|
}, [isProjectPickerOpen]);
|
||||||
|
|
||||||
// Sensors for drag-and-drop
|
// Drag-and-drop for project reordering
|
||||||
const sensors = useSensors(
|
const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects });
|
||||||
useSensor(PointerSensor, {
|
|
||||||
activationConstraint: {
|
|
||||||
distance: 5, // Small distance to start drag
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle drag end for reordering projects
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
|
||||||
const oldIndex = projects.findIndex((p) => p.id === active.id);
|
|
||||||
const newIndex = projects.findIndex((p) => p.id === over.id);
|
|
||||||
|
|
||||||
if (oldIndex !== -1 && newIndex !== -1) {
|
|
||||||
reorderProjects(oldIndex, newIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projects, reorderProjects]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Subscribe to spec regeneration events
|
// Subscribe to spec regeneration events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
3
apps/ui/src/components/layout/sidebar/hooks/index.ts
Normal file
3
apps/ui/src/components/layout/sidebar/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { useThemePreview } from './use-theme-preview';
|
||||||
|
export { useSidebarAutoCollapse } from './use-sidebar-auto-collapse';
|
||||||
|
export { useDragAndDrop } from './use-drag-and-drop';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useSensors, useSensor, PointerSensor, type DragEndEvent } from '@dnd-kit/core';
|
||||||
|
import type { Project } from '@/lib/electron';
|
||||||
|
|
||||||
|
interface UseDragAndDropProps {
|
||||||
|
projects: Project[];
|
||||||
|
reorderProjects: (oldIndex: number, newIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDragAndDrop({ projects, reorderProjects }: UseDragAndDropProps) {
|
||||||
|
// Sensors for drag-and-drop
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 5, // Small distance to start drag
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle drag end for reordering projects
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
const oldIndex = projects.findIndex((p) => p.id === active.id);
|
||||||
|
const newIndex = projects.findIndex((p) => p.id === over.id);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
reorderProjects(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[projects, reorderProjects]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sensors,
|
||||||
|
handleDragEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface UseSidebarAutoCollapseProps {
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarAutoCollapse({
|
||||||
|
sidebarOpen,
|
||||||
|
toggleSidebar,
|
||||||
|
}: UseSidebarAutoCollapseProps) {
|
||||||
|
// Auto-collapse sidebar on small screens
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia('(max-width: 1024px)'); // lg breakpoint
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (mediaQuery.matches && sidebarOpen) {
|
||||||
|
// Auto-collapse on small screens
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check on mount
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
mediaQuery.addEventListener('change', handleResize);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleResize);
|
||||||
|
}, [sidebarOpen, toggleSidebar]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
import type { ThemeMode } from '@/store/app-store';
|
||||||
|
|
||||||
|
interface UseThemePreviewProps {
|
||||||
|
setPreviewTheme: (theme: ThemeMode | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useThemePreview({ setPreviewTheme }: UseThemePreviewProps) {
|
||||||
|
// Debounced preview theme handlers to prevent excessive re-renders
|
||||||
|
const previewTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const handlePreviewEnter = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
// Clear any pending timeout
|
||||||
|
if (previewTimeoutRef.current) {
|
||||||
|
clearTimeout(previewTimeoutRef.current);
|
||||||
|
}
|
||||||
|
// Small delay to debounce rapid hover changes
|
||||||
|
previewTimeoutRef.current = setTimeout(() => {
|
||||||
|
setPreviewTheme(value as ThemeMode);
|
||||||
|
}, 16); // ~1 frame delay
|
||||||
|
},
|
||||||
|
[setPreviewTheme]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePreviewLeave = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (!relatedTarget?.closest('[data-testid^="project-theme-"]')) {
|
||||||
|
// Clear any pending timeout
|
||||||
|
if (previewTimeoutRef.current) {
|
||||||
|
clearTimeout(previewTimeoutRef.current);
|
||||||
|
}
|
||||||
|
setPreviewTheme(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setPreviewTheme]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (previewTimeoutRef.current) {
|
||||||
|
clearTimeout(previewTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handlePreviewEnter,
|
||||||
|
handlePreviewLeave,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user