feat(site): responsive mobile nav, css-driven theme icons, localizePath helper

This commit is contained in:
Christian Visintin
2026-06-07 21:50:21 +02:00
parent d670aed079
commit a25b17a4e5
5 changed files with 47 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
--- ---
import { locales, defaultLocale, type Locale } from "../i18n/ui"; import { locales, localizePath, type Locale } from "../i18n/ui";
interface Props { interface Props {
locale: Locale; locale: Locale;
@@ -13,7 +13,7 @@ const labels: Record<Locale, string> = {
fr: "FR", fr: "FR",
es: "ES", es: "ES",
}; };
const href = (l: Locale) => (l === defaultLocale ? path : `/${l}${path}`); const href = (l: Locale) => localizePath(l, path);
--- ---
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
{locales.map((l) => ( {locales.map((l) => (

View File

@@ -1,5 +1,5 @@
--- ---
import { defaultLocale, useTranslations, type Locale } from "../i18n/ui"; import { localizePath, useTranslations, type Locale } from "../i18n/ui";
import ThemeToggle from "./ThemeToggle.astro"; import ThemeToggle from "./ThemeToggle.astro";
import LangPicker from "./LangPicker.astro"; import LangPicker from "./LangPicker.astro";
@@ -9,7 +9,7 @@ interface Props {
} }
const { locale, path } = Astro.props; const { locale, path } = Astro.props;
const t = useTranslations(locale); const t = useTranslations(locale);
const p = (sub: string) => (locale === defaultLocale ? sub : `/${locale}${sub}`); const p = (sub: string) => localizePath(locale, sub);
--- ---
<header class="sticky top-0 z-50 border-b border-line bg-mantle/90 backdrop-blur"> <header class="sticky top-0 z-50 border-b border-line bg-mantle/90 backdrop-blur">
<nav class="mx-auto flex max-w-5xl items-center justify-between px-4 py-3"> <nav class="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
@@ -17,12 +17,26 @@ const p = (sub: string) => (locale === defaultLocale ? sub : `/${locale}${sub}`)
<img src="/assets/images/termscp.webp" alt="termscp" class="h-7 w-7" /> <img src="/assets/images/termscp.webp" alt="termscp" class="h-7 w-7" />
<span class="font-bold">termscp</span> <span class="font-bold">termscp</span>
</a> </a>
<div class="flex items-center gap-5 text-sm">
<!-- Desktop -->
<div class="hidden items-center gap-5 text-sm sm:flex">
<a href={p("/install")} class="text-overlay hover:text-text">{t("nav.install")}</a> <a href={p("/install")} class="text-overlay hover:text-text">{t("nav.install")}</a>
<a href={p("/user-manual")} class="text-overlay hover:text-text">{t("nav.manual")}</a> <a href={p("/user-manual")} class="text-overlay hover:text-text">{t("nav.manual")}</a>
<a href="https://github.com/veeso/termscp" class="text-overlay hover:text-text">{t("nav.github")}</a> <a href="https://github.com/veeso/termscp" class="text-overlay hover:text-text">{t("nav.github")}</a>
<LangPicker locale={locale} path={path} /> <LangPicker locale={locale} path={path} />
<ThemeToggle /> <ThemeToggle />
</div> </div>
<!-- Mobile -->
<details class="relative sm:hidden">
<summary class="cursor-pointer list-none rounded p-2 text-text hover:bg-mantle" aria-label="Menu">☰</summary>
<div class="absolute right-0 mt-2 flex w-48 flex-col gap-3 rounded-lg border border-line bg-mantle p-4 text-sm shadow-xl">
<a href={p("/install")} class="text-overlay hover:text-text">{t("nav.install")}</a>
<a href={p("/user-manual")} class="text-overlay hover:text-text">{t("nav.manual")}</a>
<a href="https://github.com/veeso/termscp" class="text-overlay hover:text-text">{t("nav.github")}</a>
<LangPicker locale={locale} path={path} />
<ThemeToggle />
</div>
</details>
</nav> </nav>
</header> </header>

View File

@@ -1,32 +1,31 @@
--- ---
// Tiny client island: flips data-theme + persists. Contract: writes "frappe"|"latte". // Theme toggle. Contract: localStorage 'theme' = "frappe" | "latte". Safe to render multiple times.
--- ---
<button <button
id="theme-toggle"
type="button" type="button"
data-theme-toggle
aria-label="Toggle theme" aria-label="Toggle theme"
aria-pressed="false"
class="rounded p-2 text-text hover:bg-mantle" class="rounded p-2 text-text hover:bg-mantle"
> >
<span data-icon="frappe">☾</span> <span data-icon="frappe">☾</span>
<span data-icon="latte" hidden>☀</span> <span data-icon="latte">☀</span>
</button> </button>
<script is:inline> <script is:inline>
(() => { (() => {
const btn = document.getElementById("theme-toggle"); if (window.__themeToggleInit) return;
const sync = () => { window.__themeToggleInit = true;
const t = document.documentElement.getAttribute("data-theme"); const root = document.documentElement;
btn.querySelector('[data-icon="frappe"]').hidden = t !== "frappe"; const btns = document.querySelectorAll("[data-theme-toggle]");
btn.querySelector('[data-icon="latte"]').hidden = t !== "latte"; const setPressed = () => btns.forEach((b) =>
}; b.setAttribute("aria-pressed", String(root.getAttribute("data-theme") === "latte")));
btn.addEventListener("click", () => { btns.forEach((b) =>
const next = b.addEventListener("click", () => {
document.documentElement.getAttribute("data-theme") === "frappe" const next = root.getAttribute("data-theme") === "frappe" ? "latte" : "frappe";
? "latte" root.setAttribute("data-theme", next);
: "frappe"; localStorage.setItem("theme", next);
document.documentElement.setAttribute("data-theme", next); setPressed();
localStorage.setItem("theme", next); }));
sync(); setPressed();
});
sync();
})(); })();
</script> </script>

View File

@@ -25,6 +25,11 @@ export function isLocale(value: string): value is Locale {
return (locales as readonly string[]).includes(value); return (locales as readonly string[]).includes(value);
} }
/** Prefix a path with the locale, except for the default locale. */
export function localizePath(locale: Locale, path: string): string {
return locale === defaultLocale ? path : `/${locale}${path}`;
}
/** Resolve a translator for `locale`, falling back to en, then to the key. */ /** Resolve a translator for `locale`, falling back to en, then to the key. */
export function useTranslations(locale: Locale) { export function useTranslations(locale: Locale) {
const dict = dictionaries[locale] ?? dictionaries[defaultLocale]; const dict = dictionaries[locale] ?? dictionaries[defaultLocale];

View File

@@ -79,6 +79,11 @@ html {
color: var(--color-text); color: var(--color-text);
} }
/* Theme toggle icons: shown based on active theme, set before paint by the head bootstrap */
[data-icon] { display: none; }
[data-theme="frappe"] [data-icon="frappe"] { display: inline; }
[data-theme="latte"] [data-icon="latte"] { display: inline; }
.blink { .blink {
display: inline-block; display: inline-block;
width: 0.5rem; width: 0.5rem;