feat(ui): add per-project themes and fix scrolling/styling issues

- Add per-project theme support with theme selector in sidebar
- Fix file diffs scrolling in agent output modal with proper overflow handling
- Improve cursor styling across all interactive elements (buttons, tabs, checkboxes)
- Enhance hotkey styling to use consistent theme colors
- Fix Kanban board flash/refresh issues with React.memo optimizations
- Update tab component for better theme integration with proper active states

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-10 12:56:24 -05:00
parent 9251411da9
commit f9ba1c260a
20 changed files with 868 additions and 90 deletions

View File

@@ -301,14 +301,14 @@ export function AgentOutputModal({
</DialogHeader>
{viewMode === "changes" ? (
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-hidden">
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
{projectPath ? (
<GitDiffPanel
projectPath={projectPath}
featureId={featureId}
compact={false}
useWorktrees={useWorktrees}
className="border-0 rounded-lg h-full"
className="border-0 rounded-lg"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">

View File

@@ -268,6 +268,8 @@ export function BoardView() {
// Track previous project to detect switches
const prevProjectPathRef = useRef<string | null>(null);
const isSwitchingProjectRef = useRef<boolean>(false);
// Track if this is the initial load (to avoid showing loading spinner on subsequent reloads)
const isInitialLoadRef = useRef<boolean>(true);
// Auto mode hook
const autoMode = useAutoMode();
@@ -367,11 +369,13 @@ export function BoardView() {
const previousPath = prevProjectPathRef.current;
// If project switched, clear features first to prevent cross-contamination
// Also treat this as an initial load for the new project
if (previousPath !== null && currentPath !== previousPath) {
console.log(
`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`
);
isSwitchingProjectRef.current = true;
isInitialLoadRef.current = true;
setFeatures([]);
setPersistedCategories([]); // Also clear categories
}
@@ -379,7 +383,11 @@ export function BoardView() {
// Update the ref to track current project
prevProjectPathRef.current = currentPath;
setIsLoading(true);
// Only show loading spinner on initial load to prevent board flash during reloads
if (isInitialLoadRef.current) {
setIsLoading(true);
}
try {
const api = getElectronAPI();
const result = await api.readFile(
@@ -403,6 +411,7 @@ export function BoardView() {
console.error("Failed to load features:", error);
} finally {
setIsLoading(false);
isInitialLoadRef.current = false;
isSwitchingProjectRef.current = false;
}
}, [currentProject, setFeatures]);
@@ -1270,17 +1279,35 @@ export function BoardView() {
}
};
const getColumnFeatures = (columnId: ColumnId) => {
return features.filter((f) => {
// Memoize column features to prevent unnecessary re-renders
const columnFeaturesMap = useMemo(() => {
const map: Record<ColumnId, Feature[]> = {
backlog: [],
in_progress: [],
waiting_approval: [],
verified: [],
};
features.forEach((f) => {
// If feature has a running agent, always show it in "in_progress"
const isRunning = runningAutoTasks.includes(f.id);
if (isRunning) {
return columnId === "in_progress";
map.in_progress.push(f);
} else {
// Otherwise, use the feature's status
map[f.status].push(f);
}
// Otherwise, use the feature's status
return f.status === columnId;
});
};
return map;
}, [features, runningAutoTasks]);
const getColumnFeatures = useCallback(
(columnId: ColumnId) => {
return columnFeaturesMap[columnId];
},
[columnFeaturesMap]
);
const handleViewOutput = (feature: Feature) => {
setOutputFeature(feature);
@@ -1537,7 +1564,7 @@ export function BoardView() {
<Plus className="w-4 h-4 mr-2" />
Add Feature
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-accent border border-border-glass"
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground inline-flex items-center justify-center"
data-testid="shortcut-add-feature"
>
{ACTION_SHORTCUTS.addFeature}
@@ -2037,10 +2064,11 @@ export function BoardView() {
>
Add Feature
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20 inline-flex items-center gap-1.5"
data-testid="shortcut-confirm-add-feature"
>
<span className="leading-none flex items-center justify-center"></span>
<span className="leading-none flex items-center justify-center"></span>
</span>
</Button>
</DialogFooter>

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, memo } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
@@ -92,7 +92,7 @@ interface KanbanCardProps {
summary?: string;
}
export function KanbanCard({
export const KanbanCard = memo(function KanbanCard({
feature,
onEdit,
onDelete,
@@ -227,7 +227,7 @@ export function KanbanCard({
{/* Shortcut key badge for in-progress cards */}
{shortcutKey && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-muted border border-border text-muted-foreground z-10"
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
@@ -869,4 +869,4 @@ export function KanbanCard({
</Dialog>
</Card>
);
}
});

View File

@@ -1,5 +1,6 @@
"use client";
import { memo } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
@@ -13,7 +14,7 @@ interface KanbanColumnProps {
headerAction?: ReactNode;
}
export function KanbanColumn({
export const KanbanColumn = memo(function KanbanColumn({
id,
title,
color,
@@ -48,4 +49,4 @@ export function KanbanColumn({
</div>
</div>
);
}
});

View File

@@ -548,7 +548,7 @@ export function ProfilesView() {
<Button onClick={() => setShowAddDialog(true)} data-testid="add-profile-button" className="relative">
<Plus className="w-4 h-4 mr-2" />
New Profile
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500">
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
{ACTION_SHORTCUTS.addProfile}
</span>
</Button>