feat: App-Hülle mit Header, Theme-Umschalter und Übersicht
Layout mit Inline-Skript, das data-theme setzt, bevor der erste Pixel gemalt wird — sonst blitzt beim Laden das falsche Farbschema auf. Logo als React-Komponente statt als Bilddatei: nimmt Theme-Farben an, spart einen Netzwerkabruf und die Verläufe liegen einmalig im Root-Layout. Im hellen Modus greifen eigene Tokens für Verlaufstext und Wortmarke; der vollfarbige Signaturverlauf fällt auf warmem Weiß sonst unter WCAG AA.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Outfit } from "next/font/google";
|
||||
import { BrandDefs } from "@/components/brand/FlimmerLogo";
|
||||
import { env } from "@/lib/env";
|
||||
import "./globals.css";
|
||||
|
||||
/**
|
||||
* Outfit wird beim Bauen heruntergeladen und mit ausgeliefert — die laufende
|
||||
* Instanz fragt also niemals bei Google an.
|
||||
*/
|
||||
const outfit = Outfit({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-outfit",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(env.appUrl),
|
||||
title: {
|
||||
default: "Flimmer",
|
||||
template: "%s · Flimmer",
|
||||
},
|
||||
description: "Serien und Filme tracken — selbst gehostet, ohne fremde Konten.",
|
||||
applicationName: "Flimmer",
|
||||
manifest: "/manifest.webmanifest",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon.ico", sizes: "48x48" },
|
||||
],
|
||||
apple: "/icons/apple-touch-icon.png",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Flimmer",
|
||||
description: "Serien und Filme tracken — selbst gehostet, ohne fremde Konten.",
|
||||
images: ["/og-image.png"],
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#0c0a14" },
|
||||
{ media: "(prefers-color-scheme: light)", color: "#faf7f5" },
|
||||
],
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
viewportFit: "cover",
|
||||
};
|
||||
|
||||
/**
|
||||
* Setzt data-theme, bevor der erste Pixel gemalt wird — sonst blitzt beim
|
||||
* Laden kurz das falsche Farbschema auf.
|
||||
*/
|
||||
const THEME_BOOTSTRAP = `(function(){try{
|
||||
var stored=localStorage.getItem("flimmer-theme");
|
||||
var mode=stored==="light"||stored==="dark"?stored:
|
||||
(stored==="system"||!stored)&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";
|
||||
document.documentElement.setAttribute("data-theme",mode);
|
||||
}catch(e){document.documentElement.setAttribute("data-theme","dark");}})();`;
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="de" data-theme="dark" className={outfit.variable} suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP }} />
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<BrandDefs />
|
||||
<a
|
||||
href="#inhalt"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 fl-btn fl-btn-primary"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user