feat: Refactored SearchAutocomplete from heroSize boolean to variant st…

- "frontend/src/components/SearchAutocomplete.tsx"
- "frontend/src/App.tsx"
- "frontend/src/App.css"
- "frontend/src/pages/Home.tsx"
- "frontend/src/pages/SearchResults.tsx"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-03-31 08:42:15 +00:00
parent fa1fc82d5a
commit fea0afdec0
5 changed files with 95 additions and 8 deletions

View file

@ -1042,6 +1042,53 @@ a.app-footer__repo:hover {
border-radius: 0.625rem; border-radius: 0.625rem;
} }
/* ── Nav search variant ───────────────────────────────────────────────────── */
.search-container--nav {
position: relative;
max-width: 16rem;
margin: 0;
}
.search-form--nav {
gap: 0;
position: relative;
}
.search-input--nav {
padding: 0.375rem 2.75rem 0.375rem 0.75rem;
font-size: 0.8125rem;
border-radius: 0.375rem;
background: var(--color-bg-input);
border-color: var(--color-border);
}
.search-input--nav::placeholder {
color: var(--color-text-muted);
opacity: 0.7;
}
.search-nav__shortcut {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.625rem;
font-family: inherit;
color: var(--color-text-muted);
background: var(--color-bg-page);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
padding: 0.0625rem 0.3125rem;
line-height: 1.4;
pointer-events: none;
}
.search-container--nav .typeahead-dropdown {
z-index: 200;
min-width: 20rem;
}
.btn--search { .btn--search {
background: var(--color-btn-search-bg); background: var(--color-btn-search-bg);
color: var(--color-btn-search-text); color: var(--color-btn-search-text);

View file

@ -1,4 +1,4 @@
import { Link, Navigate, Route, Routes } from "react-router-dom"; import { Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import Home from "./pages/Home"; import Home from "./pages/Home";
import SearchResults from "./pages/SearchResults"; import SearchResults from "./pages/SearchResults";
import TechniquePage from "./pages/TechniquePage"; import TechniquePage from "./pages/TechniquePage";
@ -11,14 +11,27 @@ import AdminPipeline from "./pages/AdminPipeline";
import About from "./pages/About"; import About from "./pages/About";
import AdminDropdown from "./components/AdminDropdown"; import AdminDropdown from "./components/AdminDropdown";
import AppFooter from "./components/AppFooter"; import AppFooter from "./components/AppFooter";
import SearchAutocomplete from "./components/SearchAutocomplete";
export default function App() { export default function App() {
const location = useLocation();
const navigate = useNavigate();
const showNavSearch = location.pathname !== "/";
return ( return (
<div className="app"> <div className="app">
<header className="app-header"> <header className="app-header">
<Link to="/" className="app-header__brand"> <Link to="/" className="app-header__brand">
<h1>Chrysopedia</h1> <h1>Chrysopedia</h1>
</Link> </Link>
{showNavSearch && (
<SearchAutocomplete
variant="nav"
globalShortcut
placeholder="Search… ⌘K"
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
/>
)}
<div className="app-header__right"> <div className="app-header__right">
<nav className="app-nav"> <nav className="app-nav">
<Link to="/">Home</Link> <Link to="/">Home</Link>

View file

@ -19,18 +19,26 @@ import {
interface SearchAutocompleteProps { interface SearchAutocompleteProps {
onSearch: (query: string) => void; onSearch: (query: string) => void;
placeholder?: string; placeholder?: string;
/** @deprecated Use variant="hero" instead */
heroSize?: boolean; heroSize?: boolean;
variant?: 'hero' | 'inline' | 'nav';
initialQuery?: string; initialQuery?: string;
autoFocus?: boolean; autoFocus?: boolean;
/** When true, Cmd+K / Ctrl+K focuses the input globally */
globalShortcut?: boolean;
} }
export default function SearchAutocomplete({ export default function SearchAutocomplete({
onSearch, onSearch,
placeholder = "Search techniques…", placeholder = "Search techniques…",
heroSize = false, heroSize = false,
variant: variantProp,
initialQuery = "", initialQuery = "",
autoFocus = false, autoFocus = false,
globalShortcut = false,
}: SearchAutocompleteProps) { }: SearchAutocompleteProps) {
// Resolve variant: explicit prop wins, then legacy heroSize, then default 'inline'
const variant = variantProp ?? (heroSize ? 'hero' : 'inline');
const [query, setQuery] = useState(initialQuery); const [query, setQuery] = useState(initialQuery);
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]); const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
const [popularSuggestions, setPopularSuggestions] = useState<SuggestionItem[]>([]); const [popularSuggestions, setPopularSuggestions] = useState<SuggestionItem[]>([]);
@ -64,6 +72,19 @@ export default function SearchAutocomplete({
if (autoFocus) inputRef.current?.focus(); if (autoFocus) inputRef.current?.focus();
}, [autoFocus]); }, [autoFocus]);
// Global Cmd+K / Ctrl+K shortcut to focus input
useEffect(() => {
if (!globalShortcut) return;
function handleGlobalKey(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
inputRef.current?.focus();
}
}
document.addEventListener("keydown", handleGlobalKey);
return () => document.removeEventListener("keydown", handleGlobalKey);
}, [globalShortcut]);
// Close dropdown on outside click // Close dropdown on outside click
useEffect(() => { useEffect(() => {
function handleClick(e: MouseEvent) { function handleClick(e: MouseEvent) {
@ -145,15 +166,15 @@ export default function SearchAutocomplete({
}; };
return ( return (
<div className="search-container" ref={dropdownRef}> <div className={`search-container${variant === 'nav' ? ' search-container--nav' : ''}`} ref={dropdownRef}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={`search-form ${heroSize ? "search-form--hero" : "search-form--inline"}`} className={`search-form search-form--${variant}`}
> >
<input <input
ref={inputRef} ref={inputRef}
type="search" type="search"
className={`search-input ${heroSize ? "search-input--hero" : "search-input--inline"}`} className={`search-input search-input--${variant}`}
placeholder={placeholder} placeholder={placeholder}
value={query} value={query}
onChange={(e) => handleInputChange(e.target.value)} onChange={(e) => handleInputChange(e.target.value)}
@ -161,9 +182,14 @@ export default function SearchAutocomplete({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-label="Search techniques" aria-label="Search techniques"
/> />
{variant !== 'nav' && (
<button type="submit" className="btn btn--search"> <button type="submit" className="btn btn--search">
Search Search
</button> </button>
)}
{variant === 'nav' && (
<kbd className="search-nav__shortcut">K</kbd>
)}
</form> </form>
{showDropdown && (showPopular || showSearch) && ( {showDropdown && (showPopular || showSearch) && (

View file

@ -104,7 +104,7 @@ export default function Home() {
</p> </p>
<SearchAutocomplete <SearchAutocomplete
heroSize variant="hero"
autoFocus autoFocus
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)} onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
/> />

View file

@ -54,6 +54,7 @@ export default function SearchResults() {
<div className="search-results-page"> <div className="search-results-page">
{/* Inline search bar */} {/* Inline search bar */}
<SearchAutocomplete <SearchAutocomplete
variant="inline"
initialQuery={q} initialQuery={q}
onSearch={(newQ) => onSearch={(newQ) =>
navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true }) navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true })