mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
♻️ refactor: implement Phase 3 sidebar refactoring (partial)
Extract inline components and organize sidebar structure: - Create sidebar/ subfolder structure (components/, hooks/, dialogs/) - Extract types.ts: NavSection, NavItem, component prop interfaces - Extract constants.ts: theme options, feature flags - Extract 3 inline components into separate files: - sortable-project-item.tsx (drag-and-drop project item) - theme-menu-item.tsx (memoized theme selector) - bug-report-button.tsx (reusable bug report button) - Update sidebar.tsx to import from extracted modules - Reduce sidebar.tsx from 2323 to 2187 lines (-136 lines) This is Phase 3 (partial) of folder-pattern.md compliance: breaking down the monolithic sidebar.tsx into maintainable, reusable components. Further refactoring (hooks extraction, dialog extraction) can be done incrementally to avoid disrupting functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,159 +83,18 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
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';
|
||||||
|
|
||||||
interface NavSection {
|
// Local imports from subfolder
|
||||||
label?: string;
|
import type { NavSection, NavItem } from './sidebar/types';
|
||||||
items: NavItem[];
|
import { SortableProjectItem, ThemeMenuItem, BugReportButton } from './sidebar/components';
|
||||||
}
|
import {
|
||||||
|
PROJECT_DARK_THEMES,
|
||||||
interface NavItem {
|
PROJECT_LIGHT_THEMES,
|
||||||
id: string;
|
SIDEBAR_FEATURE_FLAGS,
|
||||||
label: string;
|
} from './sidebar/constants';
|
||||||
icon: any;
|
|
||||||
shortcut?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortable Project Item Component
|
|
||||||
interface SortableProjectItemProps {
|
|
||||||
project: Project;
|
|
||||||
currentProjectId: string | undefined;
|
|
||||||
isHighlighted: boolean;
|
|
||||||
onSelect: (project: Project) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SortableProjectItem({
|
|
||||||
project,
|
|
||||||
currentProjectId,
|
|
||||||
isHighlighted,
|
|
||||||
onSelect,
|
|
||||||
}: SortableProjectItemProps) {
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
||||||
id: project.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200',
|
|
||||||
'text-muted-foreground hover:text-foreground hover:bg-accent/80',
|
|
||||||
isDragging && 'bg-accent shadow-lg scale-[1.02]',
|
|
||||||
isHighlighted && 'bg-brand-500/10 text-foreground ring-1 ring-brand-500/20'
|
|
||||||
)}
|
|
||||||
data-testid={`project-option-${project.id}`}
|
|
||||||
>
|
|
||||||
{/* Drag Handle */}
|
|
||||||
<button
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
className="p-0.5 rounded-md hover:bg-accent/50 cursor-grab active:cursor-grabbing transition-colors"
|
|
||||||
data-testid={`project-drag-handle-${project.id}`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/60" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Project content - clickable area */}
|
|
||||||
<div className="flex items-center gap-2.5 flex-1 min-w-0" onClick={() => onSelect(project)}>
|
|
||||||
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="flex-1 truncate text-sm font-medium">{project.name}</span>
|
|
||||||
{currentProjectId === project.id && <Check className="h-4 w-4 text-brand-500 shrink-0" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme options for project theme selector - derived from the shared config
|
|
||||||
import { darkThemes, lightThemes } from '@/config/theme-options';
|
|
||||||
|
|
||||||
const PROJECT_DARK_THEMES = darkThemes.map((opt) => ({
|
|
||||||
value: opt.value,
|
|
||||||
label: opt.label,
|
|
||||||
icon: opt.Icon,
|
|
||||||
color: opt.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PROJECT_LIGHT_THEMES = lightThemes.map((opt) => ({
|
|
||||||
value: opt.value,
|
|
||||||
label: opt.label,
|
|
||||||
icon: opt.Icon,
|
|
||||||
color: opt.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Memoized theme menu item to prevent re-renders during hover
|
|
||||||
interface ThemeMenuItemProps {
|
|
||||||
option: {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
onPreviewEnter: (value: string) => void;
|
|
||||||
onPreviewLeave: (e: React.PointerEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeMenuItem = memo(function ThemeMenuItem({
|
|
||||||
option,
|
|
||||||
onPreviewEnter,
|
|
||||||
onPreviewLeave,
|
|
||||||
}: ThemeMenuItemProps) {
|
|
||||||
const Icon = option.icon;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.value}
|
|
||||||
onPointerEnter={() => onPreviewEnter(option.value)}
|
|
||||||
onPointerLeave={onPreviewLeave}
|
|
||||||
>
|
|
||||||
<DropdownMenuRadioItem
|
|
||||||
value={option.value}
|
|
||||||
data-testid={`project-theme-${option.value}`}
|
|
||||||
className="text-xs py-1.5"
|
|
||||||
>
|
|
||||||
<Icon className="w-3.5 h-3.5 mr-1.5" style={{ color: option.color }} />
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reusable Bug Report Button Component
|
|
||||||
const BugReportButton = ({
|
|
||||||
sidebarExpanded,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
sidebarExpanded: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
'titlebar-no-drag px-3 py-2.5 rounded-xl',
|
|
||||||
'text-muted-foreground hover:text-foreground hover:bg-accent/80',
|
|
||||||
'border border-transparent hover:border-border/40',
|
|
||||||
'transition-all duration-200 ease-out',
|
|
||||||
'hover:scale-[1.02] active:scale-[0.97]',
|
|
||||||
sidebarExpanded && 'absolute right-3'
|
|
||||||
)}
|
|
||||||
title="Report Bug / Feature Request"
|
|
||||||
data-testid={sidebarExpanded ? 'bug-report-link' : 'bug-report-link-collapsed'}
|
|
||||||
>
|
|
||||||
<Bug className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -267,12 +126,8 @@ export function Sidebar() {
|
|||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Environment variable flags for hiding sidebar items
|
// Environment variable flags for hiding sidebar items
|
||||||
const hideTerminal = import.meta.env.VITE_HIDE_TERMINAL === 'true';
|
const { hideTerminal, hideWiki, hideRunningAgents, hideContext, hideSpecEditor, hideAiProfiles } =
|
||||||
const hideWiki = import.meta.env.VITE_HIDE_WIKI === 'true';
|
SIDEBAR_FEATURE_FLAGS;
|
||||||
const hideRunningAgents = import.meta.env.VITE_HIDE_RUNNING_AGENTS === 'true';
|
|
||||||
const hideContext = import.meta.env.VITE_HIDE_CONTEXT === 'true';
|
|
||||||
const hideSpecEditor = import.meta.env.VITE_HIDE_SPEC_EDITOR === 'true';
|
|
||||||
const hideAiProfiles = import.meta.env.VITE_HIDE_AI_PROFILES === 'true';
|
|
||||||
|
|
||||||
// Get customizable keyboard shortcuts
|
// Get customizable keyboard shortcuts
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Bug } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { BugReportButtonProps } from '../types';
|
||||||
|
|
||||||
|
export function BugReportButton({ sidebarExpanded, onClick }: BugReportButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
'titlebar-no-drag px-3 py-2.5 rounded-xl',
|
||||||
|
'text-muted-foreground hover:text-foreground hover:bg-accent/80',
|
||||||
|
'border border-transparent hover:border-border/40',
|
||||||
|
'transition-all duration-200 ease-out',
|
||||||
|
'hover:scale-[1.02] active:scale-[0.97]',
|
||||||
|
sidebarExpanded && 'absolute right-3'
|
||||||
|
)}
|
||||||
|
title="Report Bug / Feature Request"
|
||||||
|
data-testid={sidebarExpanded ? 'bug-report-link' : 'bug-report-link-collapsed'}
|
||||||
|
>
|
||||||
|
<Bug className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { SortableProjectItem } from './sortable-project-item';
|
||||||
|
export { ThemeMenuItem } from './theme-menu-item';
|
||||||
|
export { BugReportButton } from './bug-report-button';
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { Folder, Check, GripVertical } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { SortableProjectItemProps } from '../types';
|
||||||
|
|
||||||
|
export function SortableProjectItem({
|
||||||
|
project,
|
||||||
|
currentProjectId,
|
||||||
|
isHighlighted,
|
||||||
|
onSelect,
|
||||||
|
}: SortableProjectItemProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
|
id: project.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200',
|
||||||
|
'text-muted-foreground hover:text-foreground hover:bg-accent/80',
|
||||||
|
isDragging && 'bg-accent shadow-lg scale-[1.02]',
|
||||||
|
isHighlighted && 'bg-brand-500/10 text-foreground ring-1 ring-brand-500/20'
|
||||||
|
)}
|
||||||
|
data-testid={`project-option-${project.id}`}
|
||||||
|
>
|
||||||
|
{/* Drag Handle */}
|
||||||
|
<button
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="p-0.5 rounded-md hover:bg-accent/50 cursor-grab active:cursor-grabbing transition-colors"
|
||||||
|
data-testid={`project-drag-handle-${project.id}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/60" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Project content - clickable area */}
|
||||||
|
<div className="flex items-center gap-2.5 flex-1 min-w-0" onClick={() => onSelect(project)}>
|
||||||
|
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="flex-1 truncate text-sm font-medium">{project.name}</span>
|
||||||
|
{currentProjectId === project.id && <Check className="h-4 w-4 text-brand-500 shrink-0" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { DropdownMenuRadioItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import type { ThemeMenuItemProps } from '../types';
|
||||||
|
|
||||||
|
export const ThemeMenuItem = memo(function ThemeMenuItem({
|
||||||
|
option,
|
||||||
|
onPreviewEnter,
|
||||||
|
onPreviewLeave,
|
||||||
|
}: ThemeMenuItemProps) {
|
||||||
|
const Icon = option.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
onPointerEnter={() => onPreviewEnter(option.value)}
|
||||||
|
onPointerLeave={onPreviewLeave}
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={option.value}
|
||||||
|
data-testid={`project-theme-${option.value}`}
|
||||||
|
className="text-xs py-1.5"
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5 mr-1.5" style={{ color: option.color }} />
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
24
apps/ui/src/components/layout/sidebar/constants.ts
Normal file
24
apps/ui/src/components/layout/sidebar/constants.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { darkThemes, lightThemes } from '@/config/theme-options';
|
||||||
|
|
||||||
|
export const PROJECT_DARK_THEMES = darkThemes.map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: opt.label,
|
||||||
|
icon: opt.Icon,
|
||||||
|
color: opt.color,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const PROJECT_LIGHT_THEMES = lightThemes.map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: opt.label,
|
||||||
|
icon: opt.Icon,
|
||||||
|
color: opt.color,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SIDEBAR_FEATURE_FLAGS = {
|
||||||
|
hideTerminal: import.meta.env.VITE_HIDE_TERMINAL === 'true',
|
||||||
|
hideWiki: import.meta.env.VITE_HIDE_WIKI === 'true',
|
||||||
|
hideRunningAgents: import.meta.env.VITE_HIDE_RUNNING_AGENTS === 'true',
|
||||||
|
hideContext: import.meta.env.VITE_HIDE_CONTEXT === 'true',
|
||||||
|
hideSpecEditor: import.meta.env.VITE_HIDE_SPEC_EDITOR === 'true',
|
||||||
|
hideAiProfiles: import.meta.env.VITE_HIDE_AI_PROFILES === 'true',
|
||||||
|
} as const;
|
||||||
36
apps/ui/src/components/layout/sidebar/types.ts
Normal file
36
apps/ui/src/components/layout/sidebar/types.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Project } from '@/lib/electron';
|
||||||
|
|
||||||
|
export interface NavSection {
|
||||||
|
label?: string;
|
||||||
|
items: NavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
shortcut?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortableProjectItemProps {
|
||||||
|
project: Project;
|
||||||
|
currentProjectId: string | undefined;
|
||||||
|
isHighlighted: boolean;
|
||||||
|
onSelect: (project: Project) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeMenuItemProps {
|
||||||
|
option: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
onPreviewEnter: (value: string) => void;
|
||||||
|
onPreviewLeave: (e: React.PointerEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BugReportButtonProps {
|
||||||
|
sidebarExpanded: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user