/** * One Font Awesome glyph per product. * * Every surface that lists products used to draw the Exxat mark for each of * them. It is the correct logo and it differentiates nothing: nine identical * `E`s in the switcher mean the user has to read every label to tell the rows * apart, and on the home the cover art was the same picture nine times. * * So the glyph names the *job* the product does, and brand identity is carried * by the colour behind it (`ProductBrandConfig.brandColor`). The Exxat mark * still owns the places where there is one product and it is the brand talking: * the sidebar lock-up, the utility-bar trigger, the login screen. * * Identity, not marketing copy — which is why this is here rather than in * `lib/mock/product-catalog.ts`. The switcher needs it and knows nothing about * taglines or screenshots. */ import type * as React from "react" import type { Product } from "@/contexts/product-context" const PRODUCT_GLYPH: Record = { "exxat-prism": "fa-light fa-stethoscope", "exxat-one-sites": "fa-light fa-hospital", "exxat-curriculum-mapping": "fa-light fa-sitemap", "exxat-compliance": "fa-light fa-shield-check", "exxat-surveys": "fa-light fa-square-poll-vertical", "exxat-exam-management": "fa-light fa-file-pen", "exxat-accreditation": "fa-light fa-award", "exxat-student-success": "fa-light fa-chart-line-up", "exxat-design-os": "fa-light fa-swatchbook", "exxat-admin": "fa-light fa-shield-halved", "exxat-people": "fa-light fa-users", "exxat-courses": "fa-light fa-books", "exxat-personnel": "fa-light fa-user-nurse", // Custom tenant products inherit Prism's IA wholesale, so they inherit its // glyph too. Their own brand colour is what tells them apart. "exxat-custom": "fa-light fa-stethoscope", } export function productGlyph(product: Product): string { return PRODUCT_GLYPH[product] ?? "fa-light fa-grid-2" } /** * The tinted square a product glyph sits in. * * A bare glyph on a menu row is the same weight as the text beside it, so a * list of them reads as one grey column and the eye has nothing to land on. * Filling a square behind it gives each row an anchor at a glance, before any * of the labels are read. * * ## Why this only hands over a custom property * * The tile used to be the brand colour at 8–20% alpha with the glyph in the raw * brand colour on top — a colour against a wash of itself. That is a fixed * perceptual distance no matter which brand you pick, and measured against * WCAG 1.4.11 it lands between 2.3:1 and 3.0:1: under the 3:1 floor for a * meaningful graphic in both themes. axe never caught it, because a Font * Awesome glyph is drawn from a pseudo-element and `color-contrast` skips * those. `scripts/icon-contrast-probe.mjs` is what measures it. * * So the tile and the glyph are pinned to opposite ends of the lightness scale * — pale tile, dark glyph in light mode and the reverse in dark — rather than * being two alphas of one colour. The brand's hue and chroma survive; only * lightness is taken over, which is the channel contrast is made of. Rules live * in `globals.css` because the two ends need a `.dark` variant and an inline * style cannot have one; this function only says which brand. * * Opaque, not translucent. The old wash composited with whatever was behind it, * so the same tile measured differently on a card, a menu, and a hover row — * three answers to a question that should have one. * * Shared by the switcher rows and the home page tiles so the same product wears * the same mark in both, and lives here rather than beside either of them * because the home page is not part of the shell the starter ships. */ export function productTileStyle(brandColor: string): React.CSSProperties { return { "--product-brand": brandColor } as React.CSSProperties } const TILE_LAYOUT = "inline-flex shrink-0 items-center justify-center" /** * Layout + surface half of {@link productTileStyle}; size and radius are the * caller's. The ring is what keeps the tile visible when the brand is pale. */ export const PRODUCT_TILE_CLASS = `product-tile ${TILE_LAYOUT} ring-1 ring-foreground/5` as const /** The glyph inside the tile. Reads `--product-brand` from the tile above it. */ export const PRODUCT_TILE_GLYPH_CLASS = "product-tile-glyph" as const /** * Same square, no brand. * * For rows that are not a product — record hubs, admin — where a brand tint * would claim an identity the row does not have. Kept here so the two kinds of * tile stay the same size and shape as each other. */ export const NEUTRAL_TILE_CLASS = `${TILE_LAYOUT} bg-muted text-muted-foreground` as const