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 // 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 // `` 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 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 ( {/* Apply persisted theme before hydration to prevent flash of wrong theme */} {children} ); }