Files
sharper.ly/apps/web/app/components/ui/Navigation/Brand.tsx
T
Illusion 2ff5cf47cf more changes
- Restore the SHARP-ER in the homepage hero section
- Match the design for the 3-image section in the homepage
2026-09-23 23:36:36 +02:00

106 lines
3.6 KiB
TypeScript

import { Link } from "react-router";
import { cx, FOCUS_RING } from "./styles";
import type { NavSurface } from "./types";
/** One half of the animated brand lockup, drawn as its own SVG file. */
interface BrandMark {
src: string;
/** Intrinsic size of the file, for the aspect ratio the browser reserves. */
width: number;
height: number;
/** The mark's own drawn width, so the pair flexes to one scale. */
drawnWidth: number;
}
/**
* The SHARP-ER lockup, split into its two drawn pieces so the "-ER" can slide
* out from behind the P and fade in once the page has settled (Navigation.css).
* Each surface ships its own export of the two pieces: white on the dark
* surface, the two-tone ink on the light surface. Both exports of a piece are
* the exact same box (218x46 and 112x46), so the pair shares one scale; each
* mark takes the share of the row its box gives it, and the suffix rides flush
* against the P's right edge.
*/
const BRAND_LOCKUP: Record<
NavSurface,
{ base: BrandMark; suffix: BrandMark }
> = {
light: {
base: { src: "/assets/sharp-wordmark.svg", width: 218, height: 46, drawnWidth: 218 },
suffix: { src: "/assets/sharp-er-suffix.svg", width: 112, height: 46, drawnWidth: 112 },
},
dark: {
base: { src: "/assets/sharp-wordmark-inverse.svg", width: 218, height: 46, drawnWidth: 218 },
suffix: { src: "/assets/sharp-er-suffix-inverse.svg", width: 112, height: 46, drawnWidth: 112 },
},
};
export interface BrandProps {
/** Locale-prefixed home path, e.g. "/en". */
href: string;
/** Accessible name of the link; the images are decorative. */
label: string;
surface: NavSurface;
/**
* Run the "-ER" reveal and its breathing loop. Off on the home page, whose
* hero plays the same reveal once - a second, looping copy beside it is
* jarring. Anywhere else the two animations run as designed.
*/
animate?: boolean;
className?: string;
}
/** The SHARP-ER wordmark, linking home. Sits at the inline start of the bar. */
export function Brand({ href, label, surface, animate = true, className }: BrandProps) {
const { base, suffix } = BRAND_LOCKUP[surface];
return (
<Link
to={href}
aria-label={label}
className={cx(
"inline-flex shrink-0 items-center rounded-control",
FOCUS_RING,
className,
)}
>
{/* `dir="ltr"` because a Latin wordmark never mirrors; its position on
the bar still follows the page's direction. */}
<span dir="ltr" className="flex w-[100px] items-start">
<img
src={base.src}
alt=""
width={base.width}
height={base.height}
loading="eager"
decoding="async"
draggable={false}
style={{ flex: `${base.drawnWidth} 1 0%` }}
className="block h-auto min-w-0"
/>
{/* The clipping box starts at the P's right edge, so the mark inside
it can slide out from behind the P (Navigation.css). Without
`animate` the box and the reveal classes are dropped and the full
lockup simply sits there. */}
<span
style={{ flex: `${suffix.drawnWidth} 1 0%` }}
className={cx("block min-w-0", animate && "sh-brand-suffix")}
>
<img
src={suffix.src}
alt=""
width={suffix.width}
height={suffix.height}
loading="eager"
decoding="async"
draggable={false}
className={cx("block h-auto w-full", animate && "sh-brand-suffix__mark")}
/>
</span>
</span>
</Link>
);
}