import type { Metadata, Viewport } from "next"; import type { CSSProperties } from "react"; import { APP_CONTENT_HEADER_HEIGHT } from "@/lib/app-chrome-layout"; import { headers } from "next/headers"; import { Inter, Instrument_Serif } from "next/font/google"; import { getLocale, getMessages, getNow, getTimeZone, getTranslations, } from "next-intl/server"; import { ThemeProvider } from "@/components/theme-provider"; import { BrowserIntlProvider } from "@/components/browser-intl-provider"; import { CookieBanner } from "@/components/cookie-banner"; import { DesktopChrome } from "@/components/desktop-chrome"; import { KeyboardModality } from "@/components/keyboard-modality"; import { PostHogInit } from "@/components/posthog-init"; import { AppLayoutInitScript } from "@/components/app-layout-init-script"; import { ThemeInitScript } from "@/components/theme-init-script"; import { TooltipProvider } from "@/components/ui/tooltip"; import { publicClientMessages } from "@/lib/public-client-messages"; import { RuntimeConfigProvider } from "@/lib/runtime-config-provider"; import { getRuntimeConfig } from "@/lib/runtime-config"; import { ACCOUNT_THEME_HEADER, isAccountTheme, } from "@/lib/account-theme"; import { parseThemeAppearanceHeaders, THEME_PRESET_HEADERS, } from "@/lib/theme-presets"; import { ogImageUrl } from "@/lib/seo"; import { SITE_NAME, SITE_URL, SITE_VERIFICATION } from "@/lib/site"; import type { Locale } from "@/i18n/config"; import "./globals.css"; // Operator-controlled metadata and client boot settings must be read when the // container starts, never while the image is built. export const dynamic = "force-dynamic"; const OPEN_GRAPH_LOCALE: Record = { en: "en_US", fr: "fr_FR", de: "de_DE", "pt-BR": "pt_BR", it: "it_IT", es: "es_ES", }; const inter = Inter({ variable: "--font-inter", subsets: ["latin"], display: "swap" }); const instrumentSerif = Instrument_Serif({ variable: "--font-instrument-serif", subsets: ["latin"], weight: "400", style: "italic", display: "swap", }); /* ── What lights up the safe areas ────────────────────── ─────────────────────── Without this export, Next serves its default viewport — `width=device-width, initial-scale=1`, sans `viewport-fit`. Or `viewport-fit: auto scales the window IN the safe zone: the page never passes under the notch or under the bar home indicator, and therefore **all `env(safe-area-inset-*)` are worth 0**. The filing writes seven — the mobile browsing pill and the clearance that it reserves (`--mobile-nav-height`, app/globals.css), the FOB of Numo, the nav marketing drawer, the wizard panel — and not a single one measured anything. `cover` makes the page full bleed and gives the insets their true value: this is the line that puts all this work already written into operation. What affects a edge of the screen must therefore, from here, carry its inset — see the block of safe areas in app/globals.css for the components/ui surfaces. We do not touch `maximumScale` NOR `userScalable`: restricting the zoom is a accessibility flaw, and none of the fixes above need it. `interactiveWidget` addresses the other half of the problem: the keyboard. By default (`resizes-visual`), the software keyboard ONLY shrinks the window visual — the layout viewport keeps its height, therefore a footer anchored in `fixed` remains stuck on a bottom of the screen which is no longer visible, and goes BEHIND the keyboard. This is the case for the two components of the product (sweater request, agent session), the same ones that `.dock-above-nav` already goes back to above the moving bar in app/globals.css. `resizes-content` shrinks the layout viewport: `h-dvh` of the shell follows, and the composer goes up with the keyboard instead of disappearing underneath. Only Chromium (Android) honors it. iOS Safari ignores the key and scrolls the whole document to reveal a focused field; `useMobileViewport` in lib/use-mobile-layout.ts cancels that scroll and makes room itself (MIN-870). `overlays-content` with the VirtualKeyboard API would only trade Android's native resize for the same manual work, so it stays `resizes-content`. */ export const viewport: Viewport = { width: "device-width", initialScale: 1, viewportFit: "cover", interactiveWidget: "resizes-content", }; export async function generateMetadata(): Promise { const [t, locale] = await Promise.all([getTranslations("Meta"), getLocale()]); return { // Base of relative URLs (canonical, OpenGraph) — without it, the pages // public declare relative `og:url`, which crawlers ignore. metadataBase: new URL(SITE_URL), // `default` applies to pages without their own title; `template` wraps // per-page titles set by nested (server) layouts, e.g. "Inbox · minddy". title: { default: SITE_NAME, template: `%s · ${SITE_NAME}` }, description: t("description"), // Sharing faults (MIN-88). Next DERIVES `twitter:*` from `openGraph`, but // nothing derives from what does not exist: without these defects, a page which // does not declare its own block — the four legal pages, /login — // left without the slightest sticker. `lib/seo.ts` replaces them page by page // on the public roads; this is the net for everything else. // // The image (MIN-512): every page WITHOUT its own social block — the whole // authenticated app, /forgot-password, /reset-password, not-found — shares // the landing's pastel thumbnail, in the reader's language. Pages that // declare their own `openGraph` replace this whole object (Next does not // merge it), which is why `lib/seo.ts` re-declares the image there. openGraph: { type: "website", siteName: "minddy", locale: OPEN_GRAPH_LOCALE[locale as Locale], url: SITE_URL, title: "minddy", description: t("description"), images: [ { url: ogImageUrl("home", locale as Locale), width: 1200, height: 630, alt: SITE_NAME, }, ], }, twitter: { card: "summary_large_image", title: "minddy", description: t("description"), images: [ogImageUrl("home", locale as Locale)], }, // Site ownership in Google Search Console / Bing Webmaster Tools. // Empty string keys are omitted: until token is stuck // in `lib/site.ts`, no empty tag is served. verification: { ...(SITE_VERIFICATION.google ? { google: SITE_VERIFICATION.google } : {}), ...(SITE_VERIFICATION.bing ? { other: { "msvalidate.01": SITE_VERIFICATION.bing } } : {}), }, // Renders . // The favicon/icon tags are auto-wired from the files in app/ (favicon.ico, // icon.tsx, icon1.png, apple-icon.tsx). The manifest is a route handler // (app/manifest.json/route.ts), which the file convention does not link. appleWebApp: { title: "minddy" }, manifest: "/manifest.json", }; } export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const [locale, messages, headerList, now, initialTimeZone] = await Promise.all([ getLocale(), getMessages(), headers(), getNow(), getTimeZone(), ]); const runtimeConfig = getRuntimeConfig().public; // Anonymous public pages (feedback board, shared views): the proxy poses // this header so that they follow the system preference instead of being forced // to dark mode like the internal app (MIN-60). const isPublicSite = headerList.get("x-minddy-public") === "1"; // The theme saved on the ACCOUNT, asserted by the proxy for app routes // (`lib/account-theme.ts`). It becomes the default everywhere — the // pre-paint script and the provider both start from it, so a new device // renders the right theme from the first pixel. Public pages stay on the // system preference: an anonymous visitor has no account to read. const rawAccountTheme = headerList.get(ACCOUNT_THEME_HEADER); const accountTheme = !isPublicSite && isAccountTheme(rawAccountTheme) ? rawAccountTheme : null; const defaultTheme: "light" | "dark" | "system" = isPublicSite ? "system" : accountTheme ?? "dark"; // The account's palettes (MIN-696): asserted by the proxy on app routes // only, so their presence is what marks a signed-in document. Public pages // and the auth screens keep the Minddy palettes. const appearance = !isPublicSite && headerList.has(THEME_PRESET_HEADERS.light) ? parseThemeAppearanceHeaders((name) => headerList.get(name)) : null; // This provider only sends the four namespaces of the public site to the browser // instead of the 67 in the catalog: the messages are a component PROP // client, therefore of the RSC inline flow, therefore 39 KB gzipped more to download // before the LCP image (MIN-100). // // **Always the same, whatever the road.** This layout chose // formerly between reduced catalog and complete catalog according to a header placed // by proxy on marketing pages — but a shared layout is NOT // re-rendered during customer navigation: part of the landing, the page // `/login` inherited the four marketing namespaces and displayed // “Auth.signIn” until the first reload. A set of messages derived from // the request is fixed at the first document; it can therefore only depend on this // which is true everywhere. // // Segments that translate elsewhere mount `FullCatalogMessages`. const clientMessages = publicClientMessages(messages); return ( {/* Theme applied BEFORE the first paint. Injected out of React tree (useServerInsertedHTML) : un