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;
}
/* ── 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 {
background: var(--color-btn-search-bg);
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 SearchResults from "./pages/SearchResults";
import TechniquePage from "./pages/TechniquePage";
@ -11,14 +11,27 @@ import AdminPipeline from "./pages/AdminPipeline";
import About from "./pages/About";
import AdminDropdown from "./components/AdminDropdown";
import AppFooter from "./components/AppFooter";
import SearchAutocomplete from "./components/SearchAutocomplete";
export default function App() {
const location = useLocation();
const navigate = useNavigate();
const showNavSearch = location.pathname !== "/";
return (
<div className="app">
<header className="app-header">
<Link to="/" className="app-header__brand">
<h1>Chrysopedia</h1>
</Link>
{showNavSearch && (
<SearchAutocomplete
variant="nav"
globalShortcut
placeholder="Search… ⌘K"
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
/>
)}
<div className="app-header__right">
<nav className="app-nav">
<Link to="/">Home</Link>

View file

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

View file

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

View file

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