feat(21-06): add conversation search input and Cmd+K shortcut
- Add Search/X icons and Input to ChatConversationList with 300ms debounce - Listen for nexus:focus-chat-search event to focus search input - Add onSearch handler to useKeyboardShortcuts (fires before input guard) - Wire Layout Cmd+K to open chat panel and dispatch focus event
This commit is contained in:
parent
d6ff48c0f3
commit
f6067ed749
3 changed files with 56 additions and 3 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus, Search, X } from "lucide-react";
|
||||
import { useChatConversations } from "../hooks/useChatConversations";
|
||||
import { useChatPanel } from "../context/ChatPanelContext";
|
||||
import { ChatConversationItem } from "./ChatConversationItem";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -22,8 +23,25 @@ interface ChatConversationListProps {
|
|||
|
||||
export function ChatConversationList({ companyId }: ChatConversationListProps) {
|
||||
const { activeConversationId, setActiveConversationId } = useChatPanel();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedSearch(searchTerm), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
// Listen for focus-chat-search custom event (dispatched by Cmd+K in Layout)
|
||||
useEffect(() => {
|
||||
const handler = () => searchInputRef.current?.focus();
|
||||
window.addEventListener("nexus:focus-chat-search", handler);
|
||||
return () => window.removeEventListener("nexus:focus-chat-search", handler);
|
||||
}, []);
|
||||
|
||||
const { data, isLoading, hasNextPage, fetchNextPage, createMutation, updateMutation, deleteMutation } =
|
||||
useChatConversations(companyId);
|
||||
useChatConversations(companyId, { search: debouncedSearch || undefined });
|
||||
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -112,6 +130,29 @@ export function ChatConversationList({ companyId }: ChatConversationListProps) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="px-2 pb-1 pt-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search conversations..."
|
||||
className="h-7 pl-7 pr-7 text-xs"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchTerm("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-1 space-y-0.5">
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function Layout() {
|
|||
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
|
||||
const { openNewIssue, openOnboarding } = useDialog();
|
||||
const { togglePanelVisible, setPanelVisible } = usePanel();
|
||||
const { chatOpen, toggleChat } = useChatPanel();
|
||||
const { chatOpen, setChatOpen, toggleChat } = useChatPanel();
|
||||
const {
|
||||
companies,
|
||||
loading: companiesLoading,
|
||||
|
|
@ -167,6 +167,10 @@ export function Layout() {
|
|||
onNewIssue: () => openNewIssue(),
|
||||
onToggleSidebar: toggleSidebar,
|
||||
onTogglePanel: togglePanel,
|
||||
onSearch: () => {
|
||||
if (!chatOpen) setChatOpen(true);
|
||||
requestAnimationFrame(() => window.dispatchEvent(new Event("nexus:focus-chat-search")));
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface ShortcutHandlers {
|
|||
onNewIssue?: () => void;
|
||||
onToggleSidebar?: () => void;
|
||||
onTogglePanel?: () => void;
|
||||
onSearch?: () => void;
|
||||
}
|
||||
|
||||
export function useKeyboardShortcuts({
|
||||
|
|
@ -18,6 +19,13 @@ export function useKeyboardShortcuts({
|
|||
if (!enabled) return;
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Cmd+K / Ctrl+K → Search (global, works even from inputs)
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
onSearch?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't fire shortcuts when typing in inputs
|
||||
if (isKeyboardShortcutTextInputTarget(e.target)) {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue