mirror of
https://github.com/veeso/termscp.git
synced 2026-07-28 18:41:11 +02:00
feat(site): responsive mobile nav, css-driven theme icons, localizePath helper
This commit is contained in:
@@ -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) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user