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:
parent
4493501d10
commit
7ab76ccafb
7 changed files with 74 additions and 21 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Siftlode</title>
|
<title>Siftlode</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
11
frontend/public/favicon.svg
Normal file
11
frontend/public/favicon.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="s" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#7c86ff"/>
|
||||||
|
<stop offset="1" stop-color="#a855f7"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="64" height="64" rx="16" fill="url(#s)"/>
|
||||||
|
<!-- Bold "S" as a path so it renders identically without depending on an installed font. -->
|
||||||
|
<path fill="#ffffff" d="M41.8 22.4c-2.2-2.5-5.6-4-9.9-4-6.3 0-10.6 3.3-10.6 8.4 0 4.9 3.6 7 9.4 8.2 4.4.9 5.9 1.7 5.9 3.5 0 1.8-1.8 2.9-4.8 2.9-3 0-5.3-1.2-7-3.3l-4.8 4c2.4 3.1 6.4 5 11.6 5 6.8 0 11.3-3.4 11.3-8.9 0-5.2-3.7-7.2-9.7-8.4-4.2-.8-5.6-1.5-5.6-3.2 0-1.6 1.6-2.6 4.2-2.6 2.6 0 4.6 1 6.1 2.8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 716 B |
|
|
@ -18,6 +18,7 @@ import {
|
||||||
type ThemePrefs,
|
type ThemePrefs,
|
||||||
} from "./lib/theme";
|
} from "./lib/theme";
|
||||||
import { hasFilterParams, isPage, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
import { hasFilterParams, isPage, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
||||||
|
import { pageTitleKey } from "./lib/pageMeta";
|
||||||
import {
|
import {
|
||||||
loadLayout,
|
loadLayout,
|
||||||
normalizeLayout,
|
normalizeLayout,
|
||||||
|
|
@ -284,6 +285,18 @@ export default function App() {
|
||||||
// Expose the current setPage to decoupled callers (download toast "View", etc.).
|
// Expose the current setPage to decoupled callers (download toast "View", etc.).
|
||||||
useEffect(() => setNavigator(setPage));
|
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) {
|
function setSidebarLayout(next: SidebarLayout) {
|
||||||
setSidebarLayoutState(next);
|
setSidebarLayoutState(next);
|
||||||
saveLayoutLocal(next);
|
saveLayoutLocal(next);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Search, X, Youtube } from "lucide-react";
|
import { Search, X, Youtube } from "lucide-react";
|
||||||
import type { FeedFilters, Me } from "../lib/api";
|
import type { FeedFilters, Me } from "../lib/api";
|
||||||
import type { Page } from "../lib/urlState";
|
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
|
// 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.
|
// 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>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 text-center text-sm font-semibold">
|
<div className="flex-1 flex justify-center">
|
||||||
{page === "stats"
|
<PageTitle label={t(pageTitleKey(page))} />
|
||||||
? 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -256,8 +256,9 @@ export default function NavSidebar({
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage("feed")}
|
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")}
|
title={t("header.feed")}
|
||||||
|
aria-label={t("header.feed")}
|
||||||
>
|
>
|
||||||
Sift<span className="text-accent">lode</span>
|
Sift<span className="text-accent">lode</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
11
frontend/src/components/PageTitle.tsx
Normal file
11
frontend/src/components/PageTitle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
frontend/src/lib/pageMeta.ts
Normal file
32
frontend/src/lib/pageMeta.ts
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue