WFDASH-WEBAPP/src/app/layout.tsx
Dave 18cdc2976e feat: show the build commit as a faint corner mark
Declares ARG NEXT_PUBLIC_APP_COMMIT, supplied by Ship, and renders it as a
nearly transparent, click-through stamp. Renders nothing when unset.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 01:27:22 +02:00

209 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Metadata, Viewport } from "next";
import localFont from "next/font/local";
import "leaflet/dist/leaflet.css";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { AuthProvider } from "@/components/auth-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
import { ThemeBootstrapScript } from "@/components/theme-bootstrap";
import { BuildStamp } from "@/components/build-stamp";
// Fonts are self-hosted from ./fonts (Latin subsets of the Google Fonts
// files, downloaded 2026-10-20). With next/font/google every build fetched
// them from fonts.googleapis.com, and the Coolify build kept failing on
// that download (Nunito/Outfit 17 Oct, Lato and JetBrains Mono 20 Oct).
// Same CSS variable names and weights as before.
// Geist (default app fonts) — variable fonts.
const geistSans = localFont({
src: "./fonts/geist-var.woff2",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/geist-mono-var.woff2",
variable: "--font-geist-mono",
weight: "100 900",
});
// Theme-bound fonts (registered as CSS vars for the theme picker to swap),
// 400 regular + 700 bold — the two weights the app uses. Most are variable
// files covering 400–700; Lato ships one file per weight.
const inter = localFont({
src: "./fonts/inter-var.woff2",
variable: "--font-inter",
weight: "400 700",
display: "swap",
});
const spaceGrotesk = localFont({
src: "./fonts/space-grotesk-var.woff2",
variable: "--font-space-grotesk",
weight: "400 700",
display: "swap",
});
const dmSans = localFont({
src: "./fonts/dm-sans-var.woff2",
variable: "--font-dm-sans",
weight: "400 700",
display: "swap",
});
const playfair = localFont({
src: "./fonts/playfair-display-var.woff2",
variable: "--font-playfair",
weight: "400 700",
display: "swap",
});
const lato = localFont({
src: [
{ path: "./fonts/lato-400.woff2", weight: "400", style: "normal" },
{ path: "./fonts/lato-700.woff2", weight: "700", style: "normal" },
],
variable: "--font-lato",
display: "swap",
});
const nunito = localFont({
src: "./fonts/nunito-var.woff2",
variable: "--font-nunito",
weight: "400 700",
display: "swap",
});
const outfit = localFont({
src: "./fonts/outfit-var.woff2",
variable: "--font-outfit",
weight: "400 700",
display: "swap",
});
const jetbrainsMono = localFont({
src: "./fonts/jetbrains-mono-var.woff2",
variable: "--font-jetbrains-mono",
weight: "400 700",
display: "swap",
});
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: "#4C96D7",
};
// `metadataBase` is required for OpenGraph / Twitter absolute URL resolution,
// otherwise Next falls back to `http://localhost:4001` in dev and emits a
// warning on every page render. We re-use the public PocketBase URL since
// the app and PB sit on the same host in dev, and in prod the same env var
// points at the deployed origin (`NEXT_PUBLIC_*` is also bundle-visible).
const METADATA_BASE =
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
(typeof process.env.NEXT_PUBLIC_POCKETBASE_URL === "string"
? process.env.NEXT_PUBLIC_POCKETBASE_URL.replace(/\/+$/, "")
: "http://localhost:4001");
export const metadata: Metadata = {
metadataBase: new URL(METADATA_BASE),
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
manifest: "/manifest.json",
// Counter PWA manifest — iOS/Android pick the most-specific match
// for the installed app icon + start_url when this is the only
// <link rel="manifest"> they recognise. The main `/manifest.json`
// stays the default for the main app.
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
{ url: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: "/apple-touch-icon.png",
},
authors: [{ name: "WFDASH" }],
keywords: ["CRM", "Business", "Quotes", "Orders", "Customers", "Suppliers"],
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "WFDASH",
},
other: {
// Next 16 renders `appleWebApp.capable: true` as the standardised
// `<meta name="mobile-web-app-capable">` and no longer emits the
// apple-prefixed name — which is the only one iOS actually reads.
//
// Without this, an "Add to Home Screen" icon is a plain Safari
// bookmark rather than a standalone app: tapping it foregrounds
// Safari on whatever tab was already open, which presents as "it
// opened the main app instead of mine". Verified missing on /m and
// /rep against the running app before this was added.
//
// Declared at the root so every surface inherits it (/, /m,
// /counter, /rep). The PWA layouts that define their own `other`
// repeat it, because a child's `other` replaces the parent's
// rather than merging into it.
"apple-mobile-web-app-capable": "yes",
},
openGraph: {
type: "website",
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
siteName: "WFDASH",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "WFDASH - Business CRM",
},
],
},
twitter: {
card: "summary_large_image",
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
images: ["/twitter-image.png"],
},
};
// All theme-bound CSS vars must be present on <html> so themes can swap them
const FONT_VARS = [
geistSans.variable,
geistMono.variable,
inter.variable,
spaceGrotesk.variable,
dmSans.variable,
playfair.variable,
lato.variable,
nunito.variable,
outfit.variable,
jetbrainsMono.variable,
].join(" ");
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
className={`${FONT_VARS} h-full antialiased`}
data-theme="wfdash"
suppressHydrationWarning
>
<head>
{/* Apply persisted theme before hydration to prevent flash of wrong theme */}
<ThemeBootstrapScript />
</head>
<body className="min-h-full flex flex-col">
<ServiceWorkerRegistration />
<ThemeProvider>
<TooltipProvider>
<AuthProvider>
{children}
<BuildStamp />
</AuthProvider>
</TooltipProvider>
<Toaster richColors />
</ThemeProvider>
</body>
</html>
);
}