// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // kit:custom-ui the HTML document and the app header, around every page // The HTML document every page is rendered into: the brand layer on , an organization's brand on // , and, on a page outside the app frame, the header with the app's name and logo. It takes its brand // as a value so a test can render it with a customer's brand and prove what the page says; the root route // (src/routes/__root.tsx) supplies the request's facts from its loader (locale, session, CSRF token, labels: // `ShellData`, F10). // // The brand layer (`brand.style`: the roles the app's colours derive, light and dark, and its `--brand-*` tokens) is // a stylesheet in whose rule (`:root:root`) outranks the kit's theme in either mode, so the first paint already // carries it: a branded app never paints the ConusAI palette first. When the platform gives the organization's theme, // that theme is the one source of the colours and the brand layer keeps only its `--brand-*` tokens (`__root.tsx`), so // the theme's own dark roles show; before, an inline brand colour on beat them. It is computed on the server, // so the browser's bundle never imports the brand package. An // organization's colours go on and not on an element below it: a menu and the phone's sheet are portals // into , and they keep its colours that way. import type { AuthBrand } from "@app/config" import type { UiLabels } from "@conusai-kit/ui/primitives" import type { CSSProperties, ReactElement, ReactNode } from "react" import { type BrowserObserveSettings, observeMeta } from "../observe/browser" import type { BoundaryStrings } from "./boundary" import type { HelpLabels } from "./help" import type { Theme } from "./preference-cookies" import { type PrivacyChoiceMessages, PrivacyChoices, PrivacyFooter } from "./privacy-choices" /** * The brand layer as a rule on the root, above the kit's theme (`:root` and `.light`) whatever order the stylesheets * load in. Its values were checked when the brand was defined (colours and lengths only); a `<` is dropped all the * same, so nothing in it can close the element. */ export function brandLayerCss(style: Readonly>): string { const declarations = Object.entries(style).map(([name, value]) => `${name}:${value}`) return declarations.length === 0 ? "" : `:root:root{${declarations.join(";").replaceAll("<", "")}}` } /** The brand without its colour roles, for a page the platform's organization theme colours: its `--brand-*` tokens. */ export function brandTokensOnly(style: Readonly>): Record { return Object.fromEntries(Object.entries(style).filter(([name]) => name.startsWith("--brand-"))) } /** The app's own share image (src/routes/api.brand.og.ts): public, because a link preview is fetched without a session. */ export const APP_SHARE_IMAGE = "/api/brand/og" /** What the document needs of the brand: its names, its logos and the token style the server computed. */ export interface ShellBrand { readonly name: string readonly shortName: string readonly logo: { readonly light: string; readonly dark: string; readonly favicon: string } /** `brandTokenStyle(brand)` of `@app/config`, computed on the server. */ readonly style: Readonly> } /** What a page says about itself to a browser tab, a share card and a search result. */ export interface BrandMetadata { readonly title: string readonly description: string readonly applicationName: string readonly icons: { readonly icon: string } readonly openGraph: { readonly siteName: string readonly title: string readonly type: "website" readonly images: readonly { readonly url: string readonly width: number readonly height: number readonly alt: string }[] } readonly twitter: { readonly card: "summary_large_image"; readonly title: string; readonly images: readonly string[] } } /** * The brand's own name, and nothing else. `image` is the share image of a page; an organization's pages pass * their own. */ export function brandMetadata( brand: Pick, image: string = APP_SHARE_IMAGE, ): BrandMetadata { return { title: brand.name, description: brand.name, applicationName: brand.name, icons: { icon: brand.logo.favicon }, openGraph: { siteName: brand.name, title: brand.name, type: "website", images: [{ url: image, width: 1200, height: 630, alt: brand.name }], }, twitter: { card: "summary_large_image", title: brand.name, images: [image] }, } } /** * `brandMetadata` as a route's `head()`: the meta tags and the icon link. `base` is the deployment's public * URL, so a link preview gets an absolute image address. */ export function brandHead( metadata: BrandMetadata, base?: string, ): { meta: Record[] links: Record[] } { const absolute = (url: string): string => (base ? new URL(url, base).href : url) const image = metadata.openGraph.images[0] return { meta: [ { charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: metadata.title }, { name: "description", content: metadata.description }, { name: "application-name", content: metadata.applicationName }, { property: "og:site_name", content: metadata.openGraph.siteName }, { property: "og:title", content: metadata.openGraph.title }, { property: "og:type", content: metadata.openGraph.type }, ...(image ? [ { property: "og:image", content: absolute(image.url) }, { property: "og:image:width", content: String(image.width) }, { property: "og:image:height", content: String(image.height) }, { property: "og:image:alt", content: image.alt }, ] : []), { name: "twitter:card", content: metadata.twitter.card }, { name: "twitter:title", content: metadata.twitter.title }, ...metadata.twitter.images.map((url) => ({ name: "twitter:image", content: absolute(url) })), ], links: [{ rel: "icon", href: metadata.icons.icon }], } } /** The labels the shell renders, translated by the root loader (src/server/page.ts `shellData`). */ export interface ShellLabels { readonly signIn: string readonly signOut: string readonly account: string readonly failed: string readonly language: string readonly languageEn: string readonly languageLt: string readonly theme: string readonly themeDark: string readonly themeLight: string readonly themeSystem: string readonly switchToLight: string readonly switchToDark: string readonly loading: string readonly notFoundTitle: string readonly notFoundBody: string readonly forbiddenTitle: string readonly forbiddenBody: string } /** The root route's loader data: everything the document and the page states render, as plain values. */ export interface ShellData { readonly brand: ShellBrand /** The way in, in the page's language (`authBrand` of `config/src/brand.ts`). */ readonly auth: AuthBrand /** The deployment's public URL: what makes a share image's address absolute for a link preview. */ readonly publicUrl: string readonly locale: string readonly theme: Theme /** The frame's layout as this browser left it (`app_sidebar`, `app_panel_width`). */ readonly sidebarOpen: boolean readonly panelWidth?: number readonly signedIn: boolean readonly csrf?: string readonly observe?: BrowserObserveSettings readonly nonce?: string /** The organization theme's stylesheet (BK12), when the platform gives one. */ readonly organizationTheme?: string readonly accountHref: string readonly labels: ShellLabels /** The strings the kit's primitives and patterns render themselves, in the page's language. */ readonly ui: UiLabels readonly help: HelpLabels readonly privacy: PrivacyChoiceMessages readonly boundary: BoundaryStrings } /** The class `system` means on this device, set before anything paints. The server cannot know it. */ const SYSTEM_THEME_SCRIPT = `document.documentElement.classList.toggle("light",matchMedia("(prefers-color-scheme: light)").matches)` export interface DocumentProps { readonly brand: ShellBrand readonly locale: string /** `light` puts the theme class on the root, the one element that carries it (spec ยง10.3). */ readonly theme: Theme readonly signedIn: boolean readonly csrf?: string /** Set when the deployment turned observability on: the browser starts its SDK from this (src/observe). */ readonly observe?: BrowserObserveSettings /** The request's script nonce: `system` needs a script in the head. */ readonly nonce?: string readonly signIn: string /** The sign-out control, already rendered with its label. */ readonly signOut: ReactNode /** The theme and language controls, already rendered with their labels. */ readonly controls: ReactNode readonly privacy: PrivacyChoiceMessages /** The organization whose page this is: its colours, and the mark a test finds it by. */ readonly tenant?: { readonly style: Readonly>; readonly mark?: string } /** The router's ``: title, meta and the stylesheet links. */ readonly head?: ReactNode /** The router's ``, last in the body. */ readonly scripts?: ReactNode readonly children: ReactNode } export function Document({ brand, locale, theme, signedIn, csrf, observe, nonce, signIn, signOut, controls, privacy, tenant, head, scripts, children, }: DocumentProps): ReactElement { return ( {theme === "system" ? (