diff --git a/src/App.tsx b/src/App.tsx index ff79fed..076f0ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { OfficialRecommendationsPage } from "./pages/OfficialRecommendations"; import { SearchPage } from "./pages/Search"; import { PostRedirect } from "./pages/PostRedirect"; import { ScrollToTop } from "./components/ScrollToTop"; +import { PageTitleProvider } from "./components/PageTitleContext"; import Favorites from "./pages/Favorites"; import { adminUiPrefix } from "./adminPaths"; import { AdminRouteTree } from "./adminRouteTree"; @@ -28,38 +29,46 @@ export default function App() { - - - - }> - } /> - } /> - } /> - } - /> - } - /> - } /> - } /> - } /> - + + + + + }> + } /> + } /> + } + /> + } + /> + } + /> + } /> + } + /> + } /> + - {adminEnabled ? ( - AdminRouteTree() - ) : ( - } - /> - )} + {adminEnabled ? ( + AdminRouteTree() + ) : ( + } + /> + )} - } /> - - + } /> + + + diff --git a/src/components/AssetStreamPage.tsx b/src/components/AssetStreamPage.tsx index a9c3983..26acb71 100644 --- a/src/components/AssetStreamPage.tsx +++ b/src/components/AssetStreamPage.tsx @@ -1,5 +1,6 @@ import type { PostScope } from "../types/post"; import { MessageStream } from "./messageStream/MessageStream"; +import { useSetPageTitle } from "./PageTitleContext"; type AssetStreamPageProps = { title: string; @@ -7,9 +8,12 @@ type AssetStreamPageProps = { }; export function AssetStreamPage({ title, scope }: AssetStreamPageProps) { + // Show the page name in the global header instead of a separate title row, + // saving vertical space. + useSetPageTitle(title); return (
- +
); } diff --git a/src/components/PageTitleContext.tsx b/src/components/PageTitleContext.tsx new file mode 100644 index 0000000..53e15ab --- /dev/null +++ b/src/components/PageTitleContext.tsx @@ -0,0 +1,41 @@ +import { + createContext, + useContext, + useEffect, + useState, + type PropsWithChildren, +} from "react"; + +type PageTitleCtx = { + title: string | null; + setTitle: (title: string | null) => void; +}; + +const PageTitleContext = createContext(null); + +/** + * Lets a page publish its title to the global header so the header can show the + * current page name (e.g. "全部资料" / "热门资料") in place of the brand, avoiding + * a separate on-page title row. Pages that don't set one fall back to the brand. + */ +export function PageTitleProvider({ children }: PropsWithChildren) { + const [title, setTitle] = useState(null); + return ( + + {children} + + ); +} + +export function usePageTitle(): string | null { + return useContext(PageTitleContext)?.title ?? null; +} + +/** Publish the current page's title; clears it again when the page unmounts. */ +export function useSetPageTitle(title: string | null): void { + const setTitle = useContext(PageTitleContext)?.setTitle; + useEffect(() => { + setTitle?.(title); + return () => setTitle?.(null); + }, [setTitle, title]); +} diff --git a/src/components/messageStream/MessageStream.tsx b/src/components/messageStream/MessageStream.tsx index ca3a86e..7e4f071 100644 --- a/src/components/messageStream/MessageStream.tsx +++ b/src/components/messageStream/MessageStream.tsx @@ -6,17 +6,15 @@ import type { PostScope } from "../../types/post"; import { Reveal } from "../../motion"; import { Skeleton } from "../Skeleton"; import { FilterChips } from "./FilterChips"; -import { SectionHeader } from "../SectionHeader"; import { MessageBubble } from "./MessageBubble"; import { useGroupedByDay } from "./hooks/useGroupedByDay"; import { usePostStream } from "./hooks/usePostStream"; export type MessageStreamProps = { scope: PostScope; - title?: string; }; -export function MessageStream({ scope, title }: MessageStreamProps) { +export function MessageStream({ scope }: MessageStreamProps) { const { t, lang } = useI18n(); const [sp, setSp] = useSearchParams(); const { hash } = useLocation(); @@ -116,14 +114,9 @@ export function MessageStream({ scope, title }: MessageStreamProps) { return (
- {/* Title + filters stay pinned below the global header so users always - see which page they're on and can switch filters while scrolling. */} + {/* Filters stay pinned below the global header (which shows the page + name) so users can switch filters while scrolling. */}
- {title ? ( -
- -
- ) : null} updateParam("type", v)} />
diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index b6611d0..c49646f 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom"; import { pageTransition } from "../motion"; import { ArkLogoMark } from "../components/ArkLogoMark"; +import { usePageTitle } from "../components/PageTitleContext"; import { BackToTop } from "../components/BackToTop"; import { DocumentMeta } from "../components/DocumentMeta"; import { SearchPanel } from "../components/SearchPanel"; @@ -289,6 +290,8 @@ export function PublicLayout() { navIsActive(pathname, search, hash, which); const isHome = pathname === "/"; const footerInContentFlow = pathname === "/browse"; + // Current page name shown in the header brand slot (falls back to the brand). + const pageTitle = usePageTitle(); const popularHref = "/browse?sort=popular"; const goSearch = () => { @@ -365,20 +368,29 @@ export function PublicLayout() {
- { - if (isHome) { - e.preventDefault(); - window.scrollTo({ top: 0, behavior: "smooth" }); - } - }} - > - - {t("brand")} - +
+ {/* Logo → home; page-name text → scroll to top of the current page. */} + { + if (isHome) { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + }} + className="shrink-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]" + > + + + +
+