feat(ui): favicon, dynamic tab title, clickable logo, styled module headers

Session-close cosmetics:
1. Favicon — an 'S' monogram (indigo→violet rounded square, path-drawn so it's font-independent)
   at public/favicon.svg, linked in index.html (tab had none before).
2. Dynamic document.title — reflects the current module ('Downloads · Siftlode'); Feed = brand
   only; an open channel page shows the channel name. Shared pageTitleKey() in lib/pageMeta.ts
   keeps the tab title and the top-bar header in lockstep.
3. The 'Siftlode' logo already navigated to Feed but read as static text — added a hover
   background + cursor affordance so it's clearly clickable.
4. Module header — new shared PageTitle component (used by every non-feed module via Header) with
   a tracked small-caps 'eyebrow' + leading accent dot (user-picked style B), styled in one place.
Verified in a real browser: favicon served (image/svg+xml), title updates per module, logo→feed,
header restyled; no console errors.
This commit is contained in:
npeter83 2026-07-04 06:17:40 +02:00
parent 4493501d10
commit 7ab76ccafb
7 changed files with 74 additions and 21 deletions

View file

@ -18,6 +18,7 @@ import {
type ThemePrefs,
} from "./lib/theme";
import { hasFilterParams, isPage, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
import { pageTitleKey } from "./lib/pageMeta";
import {
loadLayout,
normalizeLayout,
@ -284,6 +285,18 @@ export default function App() {
// Expose the current setPage to decoupled callers (download toast "View", etc.).
useEffect(() => setNavigator(setPage));
// Reflect the current module in the browser tab title so open tabs are distinguishable and the
// history reads sensibly. Feed = brand only; an open channel page shows the channel name.
useEffect(() => {
const brand = "Siftlode";
const label = channelView
? channelView.name || t("header.channelManager")
: page === "feed"
? null
: t(pageTitleKey(page));
document.title = label ? `${label} · ${brand}` : brand;
}, [page, channelView, i18n.language, t]);
function setSidebarLayout(next: SidebarLayout) {
setSidebarLayoutState(next);
saveLayoutLocal(next);

View file

@ -2,6 +2,8 @@ import { useTranslation } from "react-i18next";
import { Search, X, Youtube } from "lucide-react";
import type { FeedFilters, Me } from "../lib/api";
import type { Page } from "../lib/urlState";
import { pageTitleKey } from "../lib/pageMeta";
import PageTitle from "./PageTitle";
// Contextual top bar over the content column. Primary navigation, account and the per-user sync
// status live in the left NavSidebar; the feed's scope toggle now lives in the filter sidebar.
@ -72,26 +74,8 @@ export default function Header({
)}
</div>
) : (
<div className="flex-1 text-center text-sm font-semibold">
{page === "stats"
? t("header.usageStats")
: page === "playlists"
? t("header.account.playlists")
: page === "settings"
? t("settings.title")
: page === "scheduler"
? t("header.scheduler")
: page === "config"
? t("header.configuration")
: page === "users"
? t("header.users")
: page === "notifications"
? t("inbox.navLabel")
: page === "messages"
? t("messages.navLabel")
: page === "downloads"
? t("downloads.navLabel")
: t("header.channelManager")}
<div className="flex-1 flex justify-center">
<PageTitle label={t(pageTitleKey(page))} />
</div>
)}
</header>

View file

@ -256,8 +256,9 @@ export default function NavSidebar({
{!collapsed && (
<button
onClick={() => setPage("feed")}
className="text-lg font-bold tracking-tight select-none"
className="text-lg font-bold tracking-tight select-none cursor-pointer rounded-md -mx-1 px-1 hover:bg-card hover:opacity-90 transition"
title={t("header.feed")}
aria-label={t("header.feed")}
>
Sift<span className="text-accent">lode</span>
</button>

View file

@ -0,0 +1,11 @@
// The centered module title in the top bar. Every non-feed module renders through this one
// component, so its styling lives in a single place. Design element (user-picked): a tracked
// small-caps "eyebrow" with a leading accent dot.
export default function PageTitle({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-fg">
<span className="w-1.5 h-1.5 rounded-full bg-accent shrink-0" />
{label}
</span>
);
}

View file

@ -0,0 +1,32 @@
import type { Page } from "./urlState";
// The i18n key for a page's human title — shared by the top-bar header and the browser tab title
// so the two never drift. Keep in sync with the nav modules in NavSidebar / App's page switch.
export function pageTitleKey(page: Page): string {
switch (page) {
case "feed":
return "header.account.feed";
case "channels":
return "header.channelManager";
case "playlists":
return "header.account.playlists";
case "notifications":
return "inbox.navLabel";
case "messages":
return "messages.navLabel";
case "downloads":
return "downloads.navLabel";
case "stats":
return "header.usageStats";
case "scheduler":
return "header.scheduler";
case "config":
return "header.configuration";
case "users":
return "header.users";
case "settings":
return "settings.title";
default:
return "header.account.feed";
}
}