feat(web): move mod header into site header

Replace the global TTS Workshop header on mod routes with a ModHeader showing the thumbnail, game name, and download/setup links styled like the Search/BGM nav.
This commit is contained in:
2026-08-14 10:59:20 +08:00
parent 30ef76632f
commit a3095ead38
3 changed files with 80 additions and 47 deletions
+9 -1
View File
@@ -1,7 +1,8 @@
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { Link, Route, Routes } from 'react-router-dom'; import { Link, Route, Routes, useLocation } from 'react-router-dom';
import SearchPage from './pages/SearchPage'; import SearchPage from './pages/SearchPage';
import ModPage from './pages/ModPage'; import ModPage from './pages/ModPage';
import ModHeader from './components/ModHeader';
// The full-setup view and the bgm/tabletop pages pull in the whole three.js // The full-setup view and the bgm/tabletop pages pull in the whole three.js
// stack (Scene + all mesh viewers + @tts/tabletop). Lazy-load them so that // stack (Scene + all mesh viewers + @tts/tabletop). Lazy-load them so that
@@ -18,9 +19,15 @@ const SetupsPage = lazy(() => import('./pages/SetupsPage'));
const SetupPage = lazy(() => import('./pages/SetupPage')); const SetupPage = lazy(() => import('./pages/SetupPage'));
export default function App() { export default function App() {
const { pathname } = useLocation();
const isModRoute = pathname.startsWith('/mod/');
return ( return (
<div className="min-h-screen bg-zinc-950 text-zinc-100"> <div className="min-h-screen bg-zinc-950 text-zinc-100">
<header className="border-b border-zinc-800"> <header className="border-b border-zinc-800">
{isModRoute ? (
<ModHeader />
) : (
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-4"> <div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
<Link to="/" className="text-lg font-semibold tracking-tight"> <Link to="/" className="text-lg font-semibold tracking-tight">
TTS Workshop TTS Workshop
@@ -34,6 +41,7 @@ export default function App() {
</Link> </Link>
</nav> </nav>
</div> </div>
)}
</header> </header>
<main className="mx-auto max-w-5xl px-4 py-8"> <main className="mx-auto max-w-5xl px-4 py-8">
<Routes> <Routes>
+59
View File
@@ -0,0 +1,59 @@
import { useMemo } from 'react';
import { Link, useParams } from 'react-router-dom';
import { buildTree, collectRefs } from '@tts/extract';
import { useModStore } from '../stores/modStore';
import { useSearchStore } from '../stores/searchStore';
import { modFileUrl } from '../api';
/**
* Replaces the global site header on the mod page: thumbnail, game name, and
* a subheader with the mod id and stats, plus download/setup links styled like
* the Search/BGM nav.
*/
export default function ModHeader() {
const { id } = useParams<{ id: string }>();
const { mod } = useModStore();
const item = useSearchStore((s) => s.items.find((i) => i.id === id));
const tree = useMemo(() => (mod ? buildTree(mod.mod) : []), [mod]);
const refs = useMemo(() => (mod ? collectRefs(mod.mod) : []), [mod]);
// Prefer the metadata fetched with the save (survives a refresh); fall back
// to the search result for the brief moment before the save loads.
const title = mod?.title ?? item?.title;
const previewImageUrl = mod?.previewImageUrl ?? item?.previewImageUrl;
return (
<div className="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-4">
<div className="flex min-w-0 items-center gap-3">
{previewImageUrl && (
<img
src={previewImageUrl}
alt={title}
className="h-12 w-12 shrink-0 rounded-lg border border-zinc-800 object-cover"
/>
)}
<div className="min-w-0">
<h1 className="truncate text-lg font-semibold tracking-tight">
{title ?? `Mod ${id}`}
</h1>
{mod && (
<p className="truncate text-sm text-zinc-400">
<span className="font-mono">{id}</span> · {mod.mod.GameMode} ·{' '}
{mod.mod.Date} · {tree.length} objects · {refs.length} asset refs
</p>
)}
</div>
</div>
<nav className="flex shrink-0 gap-4 text-sm text-zinc-400">
<a
href={modFileUrl(id!, item?.fileUrl)}
className="hover:text-zinc-100"
>
Download
</a>
<Link to={`/mod/${id}/setup`} className="hover:text-zinc-100">
Setup
</Link>
</nav>
</div>
);
}
-34
View File
@@ -4,7 +4,6 @@ import { Link, useParams } from 'react-router-dom';
import { buildTree, collectRefs } from '@tts/extract'; import { buildTree, collectRefs } from '@tts/extract';
import { useModStore } from '../stores/modStore'; import { useModStore } from '../stores/modStore';
import { useSearchStore } from '../stores/searchStore'; import { useSearchStore } from '../stores/searchStore';
import { modFileUrl } from '../api';
import ObjectTree from '../components/ObjectTree'; import ObjectTree from '../components/ObjectTree';
import { ErrorBoundary } from '@tts/tabletop'; import { ErrorBoundary } from '@tts/tabletop';
import { resolveViewer } from '../components/viewers'; import { resolveViewer } from '../components/viewers';
@@ -41,39 +40,6 @@ export default function ModPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-start gap-4">
{item?.previewImageUrl && (
<img
src={item.previewImageUrl}
alt={item.title}
className="h-20 w-20 shrink-0 rounded-lg border border-zinc-800 object-cover"
/>
)}
<div className="min-w-0 flex-1">
<h1 className="truncate text-2xl font-semibold">
{item?.title ?? `Mod ${id}`}
</h1>
<p className="mt-1 text-sm text-zinc-400">
<span className="font-mono">{id}</span> · {mod.GameMode} ·{' '}
{mod.Date} · {tree.length} objects · {refs.length} asset refs
</p>
</div>
<div className="flex shrink-0 gap-2">
<a
href={modFileUrl(id!, item?.fileUrl)}
className="inline-block rounded-lg bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-300"
>
Download save file
</a>
<Link
to={`/mod/${id}/setup`}
className="inline-block rounded-lg border border-zinc-700 px-4 py-2 text-sm font-medium text-zinc-200 hover:bg-zinc-800"
>
Full setup
</Link>
</div>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[280px_1fr]"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-[280px_1fr]">
<aside className="rounded-lg border border-zinc-800 bg-zinc-900 p-2"> <aside className="rounded-lg border border-zinc-800 bg-zinc-900 p-2">
<ObjectTree <ObjectTree