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>
209 lines
7 KiB
TypeScript
209 lines
7 KiB
TypeScript
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>
|
||
);
|
||
}
|