From 39c593c454af26c98e0ae82383956e6df1abc229 Mon Sep 17 00:00:00 2001 From: TerryM Date: Sun, 31 May 2026 02:55:04 +0800 Subject: [PATCH] fix: add desktop search dropdown --- src/components/SearchPanel.tsx | 99 +++++++++++++++++++++------------- src/layouts/PublicLayout.tsx | 66 +++++++++++++++++++++-- 2 files changed, 124 insertions(+), 41 deletions(-) diff --git a/src/components/SearchPanel.tsx b/src/components/SearchPanel.tsx index dba8775..c2a2931 100644 --- a/src/components/SearchPanel.tsx +++ b/src/components/SearchPanel.tsx @@ -1,5 +1,5 @@ import { Info, Search as SearchIcon, X } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type Ref } from "react"; import { getJSON, itemsOrEmpty, readJSONCache } from "../api"; import { langQuery, type Lang } from "../i18n"; import type { Post, PostListResponse } from "../types/post"; @@ -16,6 +16,9 @@ type SearchPanelProps = { query: string; onQueryChange: (value: string) => void; onSearch: () => void; + variant?: "mobile" | "desktop"; + showInput?: boolean; + panelRef?: Ref; }; const TAG_SOURCE_LIMIT = 80; @@ -59,6 +62,9 @@ export function SearchPanel({ query, onQueryChange, onSearch, + variant = "mobile", + showInput = true, + panelRef, }: SearchPanelProps) { const inputRef = useRef(null); const [tags, setTags] = useState([]); @@ -70,12 +76,13 @@ export function SearchPanel({ const langParam = useMemo(() => langQuery(lang), [lang]); useEffect(() => { + if (!showInput) return; // Avoid scroll-into-view: browsers default-scroll the focused element // into the viewport, which moves the underlying page when the search // overlay opens from a scrolled position. `preventScroll` keeps the page // exactly where it was. inputRef.current?.focus({ preventScroll: true }); - }, []); + }, [showInput]); useEffect(() => { let cancelled = false; @@ -143,44 +150,62 @@ export function SearchPanel({ .finally(() => setIsPostLoading(false)); }; + const panelClassName = + variant === "desktop" + ? "ark-header-menu-enter fixed left-1/2 top-[72px] z-50 max-h-[min(70dvh,640px)] w-[min(720px,calc(100vw-3rem))] -translate-x-1/2 overflow-y-auto overscroll-contain rounded-3xl border border-white/10 bg-[#0f0f13] shadow-2xl shadow-black/60" + : "ark-header-menu-enter fixed inset-x-0 bottom-0 top-[64px] z-50 overflow-y-auto overscroll-contain bg-[#0f0f13] md:hidden"; + const innerClassName = + variant === "desktop" + ? "px-5 pb-6 pt-4" + : "border-t border-white/10 px-5 pb-6 pt-3 max-[360px]:px-3"; + return ( -
-
-
-
- - onQueryChange(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onSearch()} - placeholder={t("searchPanelPlaceholder")} - className="min-w-0 flex-1 bg-transparent text-base text-neutral-100 outline-none placeholder:text-[#777985]" - /> - +
+
+ {showInput ? ( + <> +
+
+ + onQueryChange(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onSearch()} + placeholder={t("searchPanelPlaceholder")} + className="min-w-0 flex-1 bg-transparent text-base text-neutral-100 outline-none placeholder:text-[#777985]" + /> + +
+ +
+ +
+ + {t("searchPanelHint")} +
+ + ) : ( +
+ + {t("searchPanelHint")}
- -
+ )} -
- - {t("searchPanelHint")} -
- -
+

{t("currentTags")} diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 83bfa68..29367b6 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -285,10 +285,13 @@ export function PublicLayout() { const outlet = useOutlet(); const [open, setOpen] = useState(false); const [mobileSearchOpen, setMobileSearchOpen] = useState(false); + const [desktopSearchOpen, setDesktopSearchOpen] = useState(false); const [q, setQ] = useState(""); const menuRef = useRef(null); const mobileMenuButtonRef = useRef(null); const desktopMenuButtonRef = useRef(null); + const desktopSearchRef = useRef(null); + const desktopSearchPanelRef = useRef(null); const nav = useNavigate(); const na = (which: PublicNavWhich) => @@ -342,6 +345,7 @@ export function PublicLayout() { nav(`/browse?q=${encodeURIComponent(s)}`); setOpen(false); setMobileSearchOpen(false); + setDesktopSearchOpen(false); }; useEffect(() => { @@ -350,6 +354,33 @@ export function PublicLayout() { ); }, [open]); + useEffect(() => { + if (!desktopSearchOpen) return; + + const closeOnOutside = (event: MouseEvent | TouchEvent) => { + const target = event.target as Node; + if ( + desktopSearchRef.current?.contains(target) || + desktopSearchPanelRef.current?.contains(target) + ) { + return; + } + setDesktopSearchOpen(false); + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setDesktopSearchOpen(false); + }; + + document.addEventListener("mousedown", closeOnOutside); + document.addEventListener("touchstart", closeOnOutside); + window.addEventListener("keydown", closeOnEscape); + return () => { + document.removeEventListener("mousedown", closeOnOutside); + document.removeEventListener("touchstart", closeOnOutside); + window.removeEventListener("keydown", closeOnEscape); + }; + }, [desktopSearchOpen]); + useEffect(() => { if (!open) return; @@ -448,6 +479,7 @@ export function PublicLayout() { type="button" onClick={() => { setOpen(false); + setDesktopSearchOpen(false); setMobileSearchOpen((value) => !value); }} className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-[#191921] outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]" @@ -470,6 +502,7 @@ export function PublicLayout() { ariaLabel={t("langLabel")} onOpen={() => { setOpen(false); + setDesktopSearchOpen(false); setMobileSearchOpen(false); }} /> @@ -479,6 +512,7 @@ export function PublicLayout() { className="inline-flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-full bg-[#191921] text-[#a8a9ae] outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]" onClick={() => { setMobileSearchOpen(false); + setDesktopSearchOpen(false); setOpen((v) => !v); }} aria-label="menu" @@ -577,10 +611,16 @@ export function PublicLayout() {
-
+
setDesktopSearchOpen(true)} + onClick={() => setDesktopSearchOpen(true)} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && goSearch()} placeholder={t("searchPlaceholder")} @@ -604,7 +646,10 @@ export function PublicLayout() { ref={desktopMenuButtonRef} type="button" className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg min-[1200px]:hidden" - onClick={() => setOpen((v) => !v)} + onClick={() => { + setDesktopSearchOpen(false); + setOpen((v) => !v); + }} aria-label="menu" > {open ? : } @@ -680,12 +725,25 @@ export function PublicLayout() { /> ) : null} + {desktopSearchOpen ? ( + + ) : null} +