Files
template-app/modules/branding/ui/brand-preview.tsx
T

87 lines
3.4 KiB
TypeScript

// What a brand looks like on each theme, and whether it is legible there: the sample of a primary
// action and of a link, drawn with the very declarations the organization layout will write, and the
// contrast numbers in words. A colour that is chosen is shown before it is saved, so "it saves but
// nobody can read it" is not a state a person can reach without being told.
import type { CSSProperties, ReactElement } from "react"
import { tenantStyle } from "../contract/style.js"
export interface PreviewLabels {
readonly title: string
readonly dark: string
readonly light: string
readonly action: string
readonly link: string
readonly sample: string
/** Contain `{ratio}`, replaced with the measured ratio to one decimal. */
readonly fillContrast: string
readonly inkContrast: string
readonly edgeContrast: string
readonly meets: string
readonly below: string
readonly adjusted: string
}
/** The default brand's own fill, so a preview with no colour chosen shows what the app already has. */
export interface PreviewProps {
readonly primary: string | null
readonly accent: string | null
/** The brand package's fill, which a preview with no colour of its own shows. */
readonly defaultPrimary: string
readonly labels: PreviewLabels
}
const AA = 4.5
/** The 3:1 that a non-text boundary needs (WCAG 1.4.11). */
const NON_TEXT = 3
const ratio = (template: string, value: number): string => template.replace("{ratio}", value.toFixed(1))
export function BrandPreview({ primary, accent, defaultPrimary, labels }: PreviewProps): ReactElement {
const { style, report } = tenantStyle({ primary: primary ?? defaultPrimary, accent })
return (
<section aria-labelledby="brand-preview-title" className="brand-preview">
<h2 id="brand-preview-title">{labels.title}</h2>
<div className="brand-preview-grid">
{(["dark", "light"] as const).map((theme) => {
const numbers = report[theme]
if (!numbers) return null
const heading = `brand-preview-${theme}`
return (
<article
key={theme}
aria-labelledby={heading}
className={theme === "light" ? "brand-preview-panel light" : "brand-preview-panel"}
style={style as CSSProperties}
>
<h3 id={heading}>{theme === "light" ? labels.light : labels.dark}</h3>
<p>{labels.sample}</p>
<button type="button" className="brand-preview-action">
{labels.action}
</button>{" "}
<a href="#brand-preview-title" className="brand-preview-link">
{labels.link}
</a>
<ul>
<li>
{ratio(labels.fillContrast, numbers.fillContrast)} (
{numbers.fillContrast >= AA ? labels.meets : labels.below})
</li>
<li>
{ratio(labels.inkContrast, numbers.inkContrast)} (
{numbers.inkContrast >= AA ? labels.meets : labels.below})
</li>
<li>
{ratio(labels.edgeContrast, numbers.edgeContrast)} (
{numbers.edgeContrast >= NON_TEXT ? labels.meets : labels.below})
</li>
</ul>
{numbers.ink.toLowerCase() !== numbers.fill.toLowerCase() ? <p>{labels.adjusted}</p> : null}
</article>
)
})}
</div>
</section>
)
}