PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
LogoMark derived width from a single hardcoded 153/189, taken from mark.png, while mark-on-dark.png was 98×112. Both call sites size the mark with height:100%/width:auto, so the rendered size came out right in the end — but the width/height attributes are the pre-load aspect hint, so the on-dark artwork reserved the wrong box and reflowed on load. Any future call site without that CSS would have rendered it stretched outright. Per-variant attributes would not have fixed it. variant="auto" is a single <img> whose srcset swaps the file underneath it, so one set of attributes has to serve both colourways; the only correct fix is for the two files to share a ratio. They also had different artwork ratios (0.810 against 0.833) and different padding, which the review did not reach: the logo changed size slightly whenever the OS theme flipped. Both are now written onto one 176×208 canvas with the artwork at the same height, centred, so they are drop-in swappable. Measured on the built output, header mark, before → after: light attr 31×38, rendered 30.75×38 → attr 32×38, rendered 32.15×38 dark attr 31×38, rendered 33.25×38 → attr 32×38, rendered 32.15×38 A 2.25px reflow and a cross-theme size jump both become 0.15px of rounding. The share card reads the same file and had its own hardcoded 45×56, which the re-crop would have stretched; it is now 47×56, with a comment tying it to the canvas. The comment on ASPECT records that re-cropping either file means re-normalising both. Verified: tsc clean, 159/159 tests, build green, and both files report naturalWidth 176 × naturalHeight 208 in the browser.
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
/**
|
||
* The schoolcompare mark.
|
||
*
|
||
* This is the supplied brand artwork, not a reconstruction of it. Two
|
||
* colourways were extracted from the logo sheet and live in public/brand:
|
||
*
|
||
* mark.png the primary lockup's mark — teal pin, white window and
|
||
* path, green leaves. Reads correctly on Warm White,
|
||
* white cards and Sand, and on both dark-theme grounds.
|
||
* mark-on-dark.png the on-dark colourway — white pin with the counter
|
||
* knocked through to the ground. Required wherever the
|
||
* ground is teal, because there the teal pin's silhouette
|
||
* disappears and only the white window survives.
|
||
*
|
||
* Both are written onto one 176×208 canvas with the artwork at the same
|
||
* height, so they are drop-in swappable — see ASPECT below for why that
|
||
* matters rather than merely being neat.
|
||
*
|
||
* `variant="auto"` serves the on-dark artwork to dark-theme viewers via a
|
||
* <picture> source, so this needs no JavaScript and no client boundary.
|
||
*
|
||
* The wordmark beside it is live text in Manrope rather than the sheet's
|
||
* raster: the written style guide specifies Manrope, and live text stays
|
||
* selectable, scales cleanly and recolours with the theme.
|
||
*
|
||
* RESOLUTION CAVEAT: the largest instance on the supplied sheet is 153×189,
|
||
* which is ample for the header (36px), the favicon and the share card, but
|
||
* short of a 512px PWA icon — that one is upscaled and is slightly soft. Drop
|
||
* a vector (SVG/AI/EPS) into public/brand and regenerate to fix it; every
|
||
* consumer goes through this component or public/brand, so it is one swap.
|
||
*/
|
||
|
||
const MARK_LIGHT = '/brand/mark.png';
|
||
const MARK_ON_DARK = '/brand/mark-on-dark.png';
|
||
|
||
/**
|
||
* Intrinsic size of the artwork files, used to derive width from height.
|
||
*
|
||
* Both colourways are deliberately written onto the SAME canvas at the same
|
||
* artwork height, so this one ratio is correct for either of them. That is not
|
||
* tidiness — it is required. `variant="auto"` renders a single <img> whose
|
||
* srcset swaps the file underneath it, so the width/height attributes are
|
||
* shared by both colourways and cannot be varied per file. When the two were
|
||
* tightly cropped they had different ratios (0.810 and 0.875) and different
|
||
* padding, which meant a wrong aspect hint before load — a reflow on load
|
||
* under `width: auto` — and a visible jump in logo size whenever the OS theme
|
||
* flipped. Re-crop or replace one file and you must re-normalise both.
|
||
*/
|
||
const ART_WIDTH = 176;
|
||
const ART_HEIGHT = 208;
|
||
const ASPECT = ART_WIDTH / ART_HEIGHT;
|
||
|
||
interface LogoMarkProps {
|
||
className?: string;
|
||
/** Rendered height in px. Width follows the artwork's aspect ratio. */
|
||
size?: number;
|
||
/**
|
||
* Which colourway to serve.
|
||
* - `auto` teal pin, swapping to the on-dark artwork in the dark theme
|
||
* - `onDark` always the white pin — for teal grounds, which are teal in
|
||
* both themes (the footer band)
|
||
*/
|
||
variant?: 'auto' | 'onDark';
|
||
title?: string;
|
||
}
|
||
|
||
export function LogoMark({ className, size = 36, variant = 'auto', title }: LogoMarkProps) {
|
||
const height = size;
|
||
const width = Math.round(size * ASPECT);
|
||
const alt = title ?? '';
|
||
|
||
if (variant === 'onDark') {
|
||
return (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
className={className}
|
||
src={MARK_ON_DARK}
|
||
width={width}
|
||
height={height}
|
||
alt={alt}
|
||
aria-hidden={title ? undefined : true}
|
||
decoding="async"
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<picture>
|
||
<source media="(prefers-color-scheme: dark)" srcSet={MARK_ON_DARK} />
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
className={className}
|
||
src={MARK_LIGHT}
|
||
width={width}
|
||
height={height}
|
||
alt={alt}
|
||
aria-hidden={title ? undefined : true}
|
||
decoding="async"
|
||
/>
|
||
</picture>
|
||
);
|
||
}
|