84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// The root layout: the document, the brand layer and the app shell (spec §10.2).
|
|
//
|
|
// The brand's `--brand-*` declarations are written as a style attribute on <html> (src/ui/document.tsx),
|
|
// which is the layer a tenant brand overrides at runtime without any stylesheet being rebuilt.
|
|
|
|
import { appBrand, publicUrlOf } from "@app/config"
|
|
import { ACCOUNT_PATH } from "@app/modules/account/contract"
|
|
import type { Metadata, Viewport } from "next"
|
|
import type { ReactNode } from "react"
|
|
import { pageContext } from "../src/server/page"
|
|
import { BoundaryMessages } from "../src/ui/boundary"
|
|
import { brandMetadata, Document } from "../src/ui/document"
|
|
import { LanguageSwitch, ThemeToggle } from "../src/ui/shell-controls"
|
|
import { SignOut } from "../src/ui/sign-out"
|
|
import "./globals.css"
|
|
|
|
/**
|
|
* The app's own name and share image. `metadataBase` makes the image's address absolute, as a link preview needs, from
|
|
* the public URL the deployment states; it is read when the page is asked for, never at build (one image, every environment).
|
|
*/
|
|
export function generateMetadata(): Metadata {
|
|
return {
|
|
...brandMetadata(appBrand),
|
|
metadataBase: new URL(publicUrlOf()),
|
|
}
|
|
}
|
|
|
|
export const viewport: Viewport = { width: "device-width", initialScale: 1 }
|
|
|
|
export default async function RootLayout({ children }: { children: ReactNode }): Promise<ReactNode> {
|
|
const { i18n, signedIn, csrf, theme, observe } = await pageContext()
|
|
const message = (key: string): string => i18n.message(key)
|
|
return (
|
|
<Document
|
|
brand={appBrand}
|
|
locale={i18n.locale}
|
|
theme={theme}
|
|
signedIn={signedIn}
|
|
{...(csrf ? { csrf } : {})}
|
|
{...(observe ? { observe } : {})}
|
|
signIn={message("app.signIn")}
|
|
signOut={
|
|
<>
|
|
<a href={ACCOUNT_PATH}>{message("account.link")}</a>
|
|
<SignOut label={message("app.signOut")} />
|
|
</>
|
|
}
|
|
controls={
|
|
<>
|
|
<LanguageSwitch
|
|
current={i18n.locale}
|
|
signedIn={signedIn}
|
|
label={message("shell.language")}
|
|
options={[
|
|
{ value: "en", label: message("shell.languageEn") },
|
|
{ value: "lt", label: message("shell.languageLt") },
|
|
]}
|
|
failedTemplate={message("app.failed")}
|
|
/>
|
|
<ThemeToggle
|
|
initial={theme}
|
|
label={message("shell.theme")}
|
|
darkLabel={message("shell.themeDark")}
|
|
lightLabel={message("shell.themeLight")}
|
|
/>
|
|
</>
|
|
}
|
|
>
|
|
<BoundaryMessages
|
|
strings={{
|
|
title: message("boundary.errorTitle"),
|
|
body: message("boundary.errorBody"),
|
|
code: message("boundary.errorCode"),
|
|
retry: message("boundary.retry"),
|
|
home: message("boundary.home"),
|
|
}}
|
|
>
|
|
{children}
|
|
</BoundaryMessages>
|
|
</Document>
|
|
)
|
|
}
|