mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Lift the light theme off the pure-black-on-white extreme. Foreground moves from a near-black L0.24 to a dark tinted neutral (foreground-on- card drops from 16.5:1 to a comfortable 13.2:1), the card leaves pure white so surfaces separate by lightness, borders lift to stay visible, and every neutral carries a faint mauve tint toward the brand hue. Adds an Alucard light value for the yellow accent so charts stop glaring. Accents, chart hues, and the dark theme are unchanged. themeColor for light matches the new background.
146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
||
import { Inter } from "next/font/google";
|
||
import { headers, cookies } from "next/headers";
|
||
import "./globals.css";
|
||
import { Providers } from "@/components/providers";
|
||
import { AuthShell } from "@/components/layout/auth-shell";
|
||
import { MonitoringBootstrap } from "@/components/monitoring/bootstrap";
|
||
import { WebVitalsReporter } from "@/components/monitoring/web-vitals-reporter";
|
||
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
|
||
import { locales, type Locale } from "@/lib/i18n/config";
|
||
import { allMessages } from "@/lib/i18n/shared-resolve";
|
||
|
||
async function resolveInitialLocale(): Promise<Locale> {
|
||
// Both cookies() and headers() can throw (DynamicServerError, etc.) —
|
||
// fall back to the default so a locale hiccup never crashes the root
|
||
// layout into global-error.tsx.
|
||
try {
|
||
const cookieStore = await cookies();
|
||
const cookieLocale = cookieStore.get("healthlog-locale")?.value;
|
||
if (cookieLocale && (locales as readonly string[]).includes(cookieLocale)) {
|
||
return cookieLocale as Locale;
|
||
}
|
||
const headerList = await headers();
|
||
return parseLocaleFromAcceptLanguage(headerList.get("accept-language"));
|
||
} catch {
|
||
return "en";
|
||
}
|
||
}
|
||
|
||
const inter = Inter({
|
||
variable: "--font-sans",
|
||
subsets: ["latin"],
|
||
weight: ["400", "500", "600", "700"],
|
||
});
|
||
|
||
export const metadata: Metadata = {
|
||
title: "HealthLog",
|
||
description:
|
||
"Self-hosted health tracker — weight, blood pressure, glucose, mood, medications. Withings + Apple Health sync, transparent derived wellness metrics, AI Insights you own.",
|
||
manifest: "/manifest.json",
|
||
icons: {
|
||
icon: "/favicon.svg",
|
||
// Pre-flattened 180×180 on the app background — Safari ignores PNG
|
||
// alpha in favourites/home-screen tiles and would render the
|
||
// transparent logo on a white slab otherwise.
|
||
apple: "/apple-touch-icon.png",
|
||
},
|
||
appleWebApp: {
|
||
capable: true,
|
||
statusBarStyle: "black-translucent",
|
||
title: "HealthLog",
|
||
},
|
||
openGraph: {
|
||
title: "HealthLog",
|
||
description:
|
||
"Self-hosted health tracker. Weight, blood pressure, glucose, mood, medications. Withings + Apple Health sync. AI Insights you own.",
|
||
type: "website",
|
||
locale: "en_US",
|
||
alternateLocale: ["de_DE", "fr_FR", "es_ES", "it_IT", "pl_PL"],
|
||
siteName: "HealthLog",
|
||
// Drop-in OG asset. Replace with a 1200×630 dashboard-screenshot
|
||
// capture when an official one ships; the logo render keeps the
|
||
// unfurl from rendering a blank tile in the meantime.
|
||
images: [
|
||
{
|
||
url: "/logo-readme.png",
|
||
width: 1000,
|
||
height: 1000,
|
||
alt: "HealthLog — your health data, your server",
|
||
},
|
||
],
|
||
},
|
||
twitter: {
|
||
card: "summary_large_image",
|
||
title: "HealthLog",
|
||
description:
|
||
"Self-hosted health tracker. Weight, blood pressure, glucose, mood, medications. Withings + Apple Health sync. AI Insights you own.",
|
||
images: ["/logo-readme.png"],
|
||
},
|
||
};
|
||
|
||
export const viewport: Viewport = {
|
||
width: "device-width",
|
||
initialScale: 1,
|
||
viewportFit: "cover",
|
||
// Match the browser chrome (Android URL bar, iOS PWA status bar) to
|
||
// the active palette. The hex values are the resolved background of
|
||
// `--background` from `app/globals.css` for each theme so the bar
|
||
// edge never seams against the page on cold paint.
|
||
themeColor: [
|
||
{ media: "(prefers-color-scheme: light)", color: "#f3f2f5" },
|
||
{ media: "(prefers-color-scheme: dark)", color: "#282a36" },
|
||
],
|
||
};
|
||
|
||
// Inline script to apply theme before first paint (prevents FOUC)
|
||
const themeScript = `(function(){try{var t=localStorage.getItem("healthlog-theme");var c=(t==="light"||t==="dark")?t:(t==="system"?(window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light"):"dark");document.documentElement.classList.add(c)}catch(e){document.documentElement.classList.add("dark")}})()`;
|
||
|
||
export default async function RootLayout({
|
||
children,
|
||
}: Readonly<{
|
||
children: React.ReactNode;
|
||
}>) {
|
||
const nonce =
|
||
process.env.NODE_ENV === "production"
|
||
? ((await headers()).get("x-nonce") ?? undefined)
|
||
: undefined;
|
||
|
||
const initialLocale = await resolveInitialLocale();
|
||
// RSC handoff for the i18n bundle split: only EN ships statically in
|
||
// the client chunk, so a non-EN first paint needs its bundle inlined
|
||
// into the payload here — that is what keeps the split free of the
|
||
// EN→DE hydration flash. EN passes nothing (the client already holds
|
||
// the static fallback floor).
|
||
const initialMessages =
|
||
initialLocale === "en" ? undefined : allMessages[initialLocale];
|
||
|
||
return (
|
||
<html lang={initialLocale} suppressHydrationWarning>
|
||
<head>
|
||
<script
|
||
suppressHydrationWarning
|
||
nonce={nonce}
|
||
dangerouslySetInnerHTML={{ __html: themeScript }}
|
||
/>
|
||
</head>
|
||
<body className={`${inter.variable} font-sans antialiased`}>
|
||
<Providers
|
||
initialLocale={initialLocale}
|
||
initialMessages={initialMessages}
|
||
>
|
||
<MonitoringBootstrap />
|
||
<WebVitalsReporter />
|
||
{/* `DEMO_MODE` is a server-only env var; the proxy uses it to
|
||
block mutations. Resolve it here (server component) and
|
||
thread the boolean into the client shell so the demo
|
||
banner can render without a client-side detection path. */}
|
||
<AuthShell demoMode={process.env.DEMO_MODE === "true"}>
|
||
{children}
|
||
</AuthShell>
|
||
</Providers>
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|