87 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|