// 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" ? (
) : null}
{head}
{csrf ? : null}
{observe ? : null}
}
>
{children}
{scripts}
)
}