refactor(board-view): extract BoardSearchBar component

This commit is contained in:
Kacper
2025-12-15 23:38:05 +01:00
parent 091c6b2737
commit 9d17cd7d9c
2 changed files with 97 additions and 51 deletions

View File

@@ -64,6 +64,7 @@ import { BoardBackgroundModal } from "@/components/dialogs/board-background-moda
import { AddFeatureDialog } from "./board-view/AddFeatureDialog";
import { EditFeatureDialog } from "./board-view/EditFeatureDialog";
import { BoardHeader } from "./board-view/BoardHeader";
import { BoardSearchBar } from "./board-view/BoardSearchBar";
import {
Plus,
RefreshCw,
@@ -285,7 +286,6 @@ export function BoardView() {
const startNextFeaturesRef = useRef<() => void>(() => {});
// Ref for search input to enable keyboard shortcut focus
const searchInputRef = useRef<HTMLInputElement>(null);
// Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
@@ -300,11 +300,6 @@ export function BoardView() {
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
{
key: "/",
action: () => searchInputRef.current?.focus(),
description: "Focus search input",
},
];
// Add shortcuts for in-progress cards (1-9 and 0 for 10th)
@@ -1735,51 +1730,13 @@ export function BoardView() {
<div className="flex-1 flex flex-col overflow-hidden">
{/* Search Bar Row */}
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
<div className="relative max-w-md flex-1 flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
ref={searchInputRef}
type="text"
placeholder="Search features by keyword..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-12 border-border"
data-testid="kanban-search-input"
/>
{searchQuery ? (
<button
onClick={() => setSearchQuery("")}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
data-testid="kanban-search-clear"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
) : (
<span
className="absolute right-2 top-1/2 -translate-y-1/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"
data-testid="kanban-search-hotkey"
>
/
</span>
)}
</div>
{/* Spec Creation Loading Badge */}
{isCreatingSpec &&
currentProject?.path === creatingSpecProjectPath && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
title="Creating App Specification"
data-testid="spec-creation-badge"
>
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
Creating spec
</span>
</div>
)}
</div>
<BoardSearchBar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isCreatingSpec={isCreatingSpec}
creatingSpecProjectPath={creatingSpecProjectPath}
currentProjectPath={currentProject?.path}
/>
{/* Board Background & Detail Level Controls */}
{isMounted && (

View File

@@ -0,0 +1,89 @@
"use client";
import { useRef, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Search, X, Loader2 } from "lucide-react";
interface BoardSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
isCreatingSpec: boolean;
creatingSpecProjectPath?: string;
currentProjectPath?: string;
}
export function BoardSearchBar({
searchQuery,
onSearchChange,
isCreatingSpec,
creatingSpecProjectPath,
currentProjectPath,
}: BoardSearchBarProps) {
const searchInputRef = useRef<HTMLInputElement>(null);
// Focus search input when "/" is pressed
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Only focus if not typing in an input/textarea
if (
e.key === "/" &&
!(e.target instanceof HTMLInputElement) &&
!(e.target instanceof HTMLTextAreaElement)
) {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<div className="relative max-w-md flex-1 flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
ref={searchInputRef}
type="text"
placeholder="Search features by keyword..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9 pr-12 border-border"
data-testid="kanban-search-input"
/>
{searchQuery ? (
<button
onClick={() => onSearchChange("")}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
data-testid="kanban-search-clear"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
) : (
<span
className="absolute right-2 top-1/2 -translate-y-1/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"
data-testid="kanban-search-hotkey"
>
/
</span>
)}
</div>
{/* Spec Creation Loading Badge */}
{isCreatingSpec &&
currentProjectPath === creatingSpecProjectPath && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
title="Creating App Specification"
data-testid="spec-creation-badge"
>
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
Creating spec
</span>
</div>
)}
</div>
);
}