Ctrl K
ComunidadGuías…

app/layout.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<Locale, string> = {
  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<Metadata> {
  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 <meta name="apple-mobile-web-app-title" content="minddy" />.
    // 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 (
    <html
      lang={locale}
      suppressHydrationWarning
      data-scroll-behavior="smooth"
      style={{ "--app-content-header-height": `${APP_CONTENT_HEADER_HEIGHT}px` } as CSSProperties}
    >
      <head>
        {/* Theme applied BEFORE the first paint. Injected out of React tree
            (useServerInsertedHTML) : un <script> rendu par un composant fait
            complaining about React 19 every time the client re-renders the root layout — see the
            composant. */}
        <AppLayoutInitScript />
        <ThemeInitScript
          defaultTheme={defaultTheme}
          accountTheme={accountTheme}
          appearance={appearance}
        />
      </head>
      <body
        className={`${inter.variable} ${instrumentSerif.variable} antialiased`}
      >
        <ThemeProvider defaultTheme={defaultTheme}>
          <TooltipProvider>
            <RuntimeConfigProvider config={runtimeConfig}>
            <BrowserIntlProvider
              locale={locale}
              messages={clientMessages}
              now={now}
              initialTimeZone={initialTimeZone}
            >
            {/* The strip by which you move the desktop app window
                (MIN-292). Here, and not in a shell: this is the only position of
                repository from where it covers ALL configurations. An audit in
                had found five without any catch — zen mode, legal pages,
                public board `/f/`, published page `/p/`, shared view `/share/`,
                plus `not-found` — because the sockets lived in the header
                and the sidebar, that is to say in the two pieces of furniture that these
                screens do not have.

                **BEFORE `{children}`, and it's not cosmetic**: Chromic
                calculates draggable regions by traversing the layout tree
                page in order, and a `no-drag` encountered LATER digs the
                `drag` encountered before. Placed last, the tape would resume
                to the buttons of the header the hole they have just dug for themselves.

                Outside of the desktop app, `globals.css` leaves it in `display: none`:
                it costs a browser nothing. */}
            <div aria-hidden className="desktop-drag-band" />
            {children}
            {/* Mark the document in the desktop app (MIN-291): this is what
                read the window movement zones, which do not have
                other possible source than the page. Don't give anything back. */}
            <DesktopChrome />
            <KeyboardModality />
            <CookieBanner />
            {/* PostHog covers public pages and the app. The SDK loads after idle
                outside the initial bundle. Device consent controls anonymous,
                persistent and disabled measurement. */}
            <PostHogInit />
            </BrowserIntlProvider>
            </RuntimeConfigProvider>
          </TooltipProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}