fix: search component and breadcrumbs fix
This commit is contained in:
@@ -86,7 +86,7 @@ export function BooksGrid({ books }: { books: BookInfo[] }) {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search books..."
|
||||
placeholder="Search books by name..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
|
||||
@@ -91,7 +91,7 @@ export function EssaysGrid({ essays }: { essays: EssayInfo[] }) {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search essays..."
|
||||
placeholder="Search essays by title..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
|
||||
@@ -82,7 +82,7 @@ export function OtherDocumentsGrid({
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search documents..."
|
||||
placeholder="Search documents by title..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
|
||||
@@ -199,9 +199,14 @@ function SearchInner() {
|
||||
selectedTypes.size === 0 ? "all" : Array.from(selectedTypes).join(",");
|
||||
const [data, setData] = useState<SearchResponse | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(!!initialQuery);
|
||||
|
||||
const debouncedQuery = useDebouncedValue(inputValue, 300);
|
||||
const isQueryEmpty = !debouncedQuery.trim();
|
||||
|
||||
// When the query is empty, derive these as "no results" during render
|
||||
// instead of setting state inside an effect.
|
||||
const effectiveData = isQueryEmpty ? null : data;
|
||||
const hasSearched = isQueryEmpty ? false : !!debouncedQuery.trim();
|
||||
|
||||
// Sync URL when state changes
|
||||
const pushUrl = useCallback(
|
||||
@@ -216,17 +221,25 @@ function SearchInner() {
|
||||
[router],
|
||||
);
|
||||
|
||||
// Reset page to 1 when query or type changes.
|
||||
// Uses the "compare previous value during render" pattern
|
||||
// recommended by React instead of a setState-in-effect.
|
||||
const [prevQuery, setPrevQuery] = useState(debouncedQuery);
|
||||
const [prevType, setPrevType] = useState(activeTypeParam);
|
||||
if (prevQuery !== debouncedQuery || prevType !== activeTypeParam) {
|
||||
setPrevQuery(debouncedQuery);
|
||||
setPrevType(activeTypeParam);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
|
||||
// Fetch search results
|
||||
useEffect(() => {
|
||||
const query = debouncedQuery.trim();
|
||||
if (!query) {
|
||||
setData(null);
|
||||
setHasSearched(false);
|
||||
pushUrl("", urlTypeParam, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
setHasSearched(true);
|
||||
setIsLoading(true);
|
||||
pushUrl(query, urlTypeParam, currentPage);
|
||||
|
||||
@@ -264,11 +277,6 @@ function SearchInner() {
|
||||
return () => controller.abort();
|
||||
}, [debouncedQuery, activeTypeParam, urlTypeParam, currentPage, pushUrl]);
|
||||
|
||||
// Reset page to 1 when query or type changes
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [debouncedQuery, activeTypeParam]);
|
||||
|
||||
// Auto-focus the input on mount
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
@@ -351,17 +359,17 @@ function SearchInner() {
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
{data &&
|
||||
{effectiveData &&
|
||||
active &&
|
||||
selectedTypes.size <= 1 &&
|
||||
data.total > 0 && (
|
||||
effectiveData.total > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-semibold leading-none",
|
||||
"bg-primary-foreground/20 text-primary-foreground",
|
||||
)}
|
||||
>
|
||||
{data.total.toLocaleString()}
|
||||
{effectiveData.total.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -391,33 +399,36 @@ function SearchInner() {
|
||||
)}
|
||||
|
||||
{/* No results */}
|
||||
{!isLoading && hasSearched && data && data.results.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Search className="size-12 text-muted-foreground/40 mb-4" />
|
||||
<p className="text-lg text-muted-foreground">
|
||||
No results found for “{debouncedQuery}”.
|
||||
<br />
|
||||
<span className="text-sm">
|
||||
Try a different search term or content type.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
hasSearched &&
|
||||
effectiveData &&
|
||||
effectiveData.results.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Search className="size-12 text-muted-foreground/40 mb-4" />
|
||||
<p className="text-lg text-muted-foreground">
|
||||
No results found for “{debouncedQuery}”.
|
||||
<br />
|
||||
<span className="text-sm">
|
||||
Try a different search term or content type.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{!isLoading && data && data.results.length > 0 && (
|
||||
{!isLoading && effectiveData && effectiveData.results.length > 0 && (
|
||||
<>
|
||||
{/* Results count + top pagination */}
|
||||
<div className="mb-4 flex items-center justify-between flex-wrap gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{data.total.toLocaleString()}{" "}
|
||||
{data.total === 1 ? "result" : "results"} for “
|
||||
{data.query}”
|
||||
{effectiveData.total.toLocaleString()}{" "}
|
||||
{effectiveData.total === 1 ? "result" : "results"} for “
|
||||
{effectiveData.query}”
|
||||
</p>
|
||||
{data.total_pages > 1 && (
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<PaginationControls
|
||||
page={data.page}
|
||||
totalPages={data.total_pages}
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
onPageChange={handlePageChange}
|
||||
compact
|
||||
/>
|
||||
@@ -426,7 +437,7 @@ function SearchInner() {
|
||||
|
||||
{/* Result list */}
|
||||
<div className="space-y-3">
|
||||
{data.results.map((result, i) => (
|
||||
{effectiveData.results.map((result, i) => (
|
||||
<ResultCard
|
||||
key={`${result.collection}-${result.id || i}`}
|
||||
result={result}
|
||||
@@ -435,12 +446,12 @@ function SearchInner() {
|
||||
</div>
|
||||
|
||||
{/* Bottom pagination */}
|
||||
{data.total_pages > 1 && (
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<div className="mt-8">
|
||||
<PaginationControls
|
||||
page={data.page}
|
||||
totalPages={data.total_pages}
|
||||
total={data.total}
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
total={effectiveData.total}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -541,7 +552,17 @@ function ResultCard({ result }: { result: SearchResult }) {
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<Link href={result.url} className="block group">
|
||||
<Link
|
||||
href={result.url}
|
||||
className="block group"
|
||||
onClick={() => {
|
||||
try {
|
||||
sessionStorage.setItem("spsays_from_search", "1");
|
||||
} catch {
|
||||
/* sessionStorage unavailable */
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card className="transition-all group-hover:shadow-md group-hover:ring-foreground/20 cursor-pointer">
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{/* Collection badge */}
|
||||
|
||||
+125
-94
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -11,22 +11,42 @@ import { cn } from "@/lib/utils";
|
||||
* into a full search input on click. Typing a query and pressing Enter
|
||||
* (or clicking the search icon again) navigates to /search?q=...
|
||||
*
|
||||
* The expansion/collapse is animated with framer-motion.
|
||||
* State is controlled by the parent (PageHeader) so the header can
|
||||
* hide/show adjacent elements (logo, reading settings) on mobile.
|
||||
*
|
||||
* Pressing Escape or clicking outside collapses the input.
|
||||
* Keyboard shortcut: "/" focuses the search from anywhere on the page.
|
||||
*/
|
||||
export function HeaderSearchButton() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
/** Map the current pathname to the search `type` filter value. */
|
||||
function searchTypeFromPath(pathname: string): string | null {
|
||||
if (pathname.startsWith("/books")) return "books";
|
||||
if (pathname.startsWith("/letters")) return "letters";
|
||||
if (pathname.startsWith("/essays")) return "essays";
|
||||
if (pathname.startsWith("/transcriptions")) return "transcriptions";
|
||||
if (pathname.startsWith("/glossary")) return "glossary";
|
||||
if (pathname.startsWith("/journals")) return "journals";
|
||||
if (pathname.startsWith("/magazines")) return "magazines";
|
||||
if (pathname.startsWith("/other-documents")) return "other_documents";
|
||||
return null; // homepage, search page, about, etc. → "all"
|
||||
}
|
||||
|
||||
export function HeaderSearchButton({
|
||||
expanded,
|
||||
onExpandedChange,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpandedChange: (expanded: boolean) => void;
|
||||
}) {
|
||||
const queryRef = useRef("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Focus input when expanded
|
||||
useEffect(() => {
|
||||
if (expanded && inputRef.current) {
|
||||
// Small delay to let the animation start before focusing
|
||||
const timer = setTimeout(() => inputRef.current?.focus(), 50);
|
||||
const timer = setTimeout(() => inputRef.current?.focus(), 80);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [expanded]);
|
||||
@@ -40,18 +60,17 @@ export function HeaderSearchButton() {
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setExpanded(false);
|
||||
onExpandedChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [expanded]);
|
||||
}, [expanded, onExpandedChange]);
|
||||
|
||||
// Global "/" shortcut to open search
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't trigger if user is typing in an input/textarea
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
@@ -64,25 +83,29 @@ export function HeaderSearchButton() {
|
||||
|
||||
if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
setExpanded(true);
|
||||
onExpandedChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
}, [onExpandedChange]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const trimmed = query.trim();
|
||||
if (trimmed) {
|
||||
router.push(`/search?q=${encodeURIComponent(trimmed)}`);
|
||||
setExpanded(false);
|
||||
setQuery("");
|
||||
} else {
|
||||
router.push("/search");
|
||||
setExpanded(false);
|
||||
}
|
||||
}, [query, router]);
|
||||
const trimmed = queryRef.current.trim();
|
||||
const params = new URLSearchParams();
|
||||
if (trimmed) params.set("q", trimmed);
|
||||
|
||||
// Pre-select the search filter matching the current section
|
||||
const type = searchTypeFromPath(pathname);
|
||||
if (type) params.set("type", type);
|
||||
|
||||
const qs = params.toString();
|
||||
router.push(`/search${qs ? `?${qs}` : ""}`);
|
||||
onExpandedChange(false);
|
||||
queryRef.current = "";
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}, [router, onExpandedChange, pathname]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -91,83 +114,91 @@ export function HeaderSearchButton() {
|
||||
handleSubmit();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
setExpanded(false);
|
||||
setQuery("");
|
||||
onExpandedChange(false);
|
||||
queryRef.current = "";
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
[handleSubmit],
|
||||
[handleSubmit, onExpandedChange],
|
||||
);
|
||||
|
||||
const collapse = useCallback(() => {
|
||||
onExpandedChange(false);
|
||||
queryRef.current = "";
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}, [onExpandedChange]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative flex items-center">
|
||||
<AnimatePresence mode="wait">
|
||||
{expanded ? (
|
||||
<motion.div
|
||||
key="search-input"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={cn(
|
||||
// Mobile: cover the entire header (absolutely positioned)
|
||||
"max-md:fixed max-md:inset-x-4 max-md:top-3 max-md:z-30",
|
||||
// Desktop: inline at 280px wide
|
||||
"md:relative md:w-[280px]",
|
||||
"flex items-center gap-2 h-9 rounded-lg border border-border bg-background px-3 overflow-hidden",
|
||||
"max-md:shadow-lg",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
aria-label="Search"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Search className="size-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search..."
|
||||
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setExpanded(false);
|
||||
setQuery("");
|
||||
}}
|
||||
aria-label="Close search"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="search-icon"
|
||||
initial={{ opacity: 0.8, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0.8, scale: 0.9 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative flex items-center",
|
||||
expanded && "max-md:flex-1 max-md:min-w-0",
|
||||
)}
|
||||
>
|
||||
{expanded ? (
|
||||
<motion.div
|
||||
key="search-input"
|
||||
initial={{ width: 36, opacity: 0 }}
|
||||
animate={{ width: "100%", opacity: 1 }}
|
||||
transition={{
|
||||
width: { type: "spring", stiffness: 400, damping: 30 },
|
||||
opacity: { duration: 0.15 },
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 h-12 md:h-9 rounded-lg border border-border bg-background px-3 overflow-hidden",
|
||||
"max-md:w-full",
|
||||
"md:w-70",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
aria-label="Search (press /)"
|
||||
title="Search (press /)"
|
||||
className={cn(
|
||||
"size-9 inline-flex items-center justify-center rounded-lg",
|
||||
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
"transition-colors outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring",
|
||||
)}
|
||||
onClick={handleSubmit}
|
||||
aria-label="Search"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Search className="size-[18px]" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Search className="size-5 md:size-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
defaultValue=""
|
||||
onChange={(e) => {
|
||||
queryRef.current = e.target.value;
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search..."
|
||||
className="flex-1 bg-transparent text-md md:text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={collapse}
|
||||
aria-label="Close search"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="search-icon"
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.12 }}
|
||||
type="button"
|
||||
onClick={() => onExpandedChange(true)}
|
||||
aria-label="Search (press /)"
|
||||
title="Search (press /)"
|
||||
className={cn(
|
||||
"size-9 inline-flex items-center justify-center rounded-lg",
|
||||
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
"transition-colors outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring",
|
||||
)}
|
||||
>
|
||||
<Search className="size-4.5" />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { ReadingSettings } from "@/components/reading-settings";
|
||||
import { HeaderSearchButton } from "@/components/header-search";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SEARCH_RETURN_KEY = "spsays_from_search";
|
||||
|
||||
export function PageHeader({
|
||||
children,
|
||||
sticky = false,
|
||||
@@ -11,6 +17,29 @@ export function PageHeader({
|
||||
sticky?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [searchExpanded, setSearchExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Read (and consume) the search-return flag once on first render.
|
||||
// The lazy initialiser runs exactly once; on the server
|
||||
// (typeof window === "undefined") it returns false.
|
||||
const [fromSearch, setFromSearch] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
if (
|
||||
sessionStorage.getItem(SEARCH_RETURN_KEY) === "1" &&
|
||||
!pathname.startsWith("/search")
|
||||
) {
|
||||
sessionStorage.removeItem(SEARCH_RETURN_KEY);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// sessionStorage unavailable
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@@ -20,10 +49,45 @@ export function PageHeader({
|
||||
)}
|
||||
>
|
||||
<div className="max-w-5xl mx-auto flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">{children}</div>
|
||||
<div className="shrink-0 pt-1 flex items-center gap-1.5">
|
||||
<HeaderSearchButton />
|
||||
<ReadingSettings />
|
||||
{/* Title / logo area — hidden on mobile when search is active */}
|
||||
<div
|
||||
className={cn("flex-1 min-w-0", searchExpanded && "max-md:hidden")}
|
||||
>
|
||||
{fromSearch ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFromSearch(false);
|
||||
router.back();
|
||||
}}
|
||||
className="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Back to search results
|
||||
</button>
|
||||
{/* Hide the page's own breadcrumb link (always the first <a>) */}
|
||||
<div className="[&>a:first-child]:hidden">{children}</div>
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions — expands to fill width on mobile when search is active */}
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 pt-1 flex items-center gap-1.5",
|
||||
searchExpanded && "max-md:flex-1 max-md:min-w-0",
|
||||
)}
|
||||
>
|
||||
<HeaderSearchButton
|
||||
expanded={searchExpanded}
|
||||
onExpandedChange={setSearchExpanded}
|
||||
/>
|
||||
{/* Hide reading settings on mobile when search is active */}
|
||||
<div className={cn(searchExpanded && "max-md:hidden")}>
|
||||
<ReadingSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -188,16 +188,20 @@ export function ReadingSettings() {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
<button
|
||||
ref={buttonRef as React.Ref<HTMLButtonElement>}
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Reading settings"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={cn(
|
||||
"size-9 inline-flex items-center justify-center rounded-lg",
|
||||
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
"transition-colors outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring",
|
||||
)}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</Button>
|
||||
<Settings className="size-5 md:size-4" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user