fix(ui): unify fonts and prevent footer text clipping

This commit is contained in:
Amruth Pillai
2026-09-30 15:11:56 +02:00
parent 51faba9e9d
commit ec0932c5d2
10 changed files with 32 additions and 50 deletions
-2
View File
@@ -32,8 +32,6 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/anybody": "^5.3.0",
"@fontsource-variable/martian-mono": "^5.3.0",
"@lezer/highlight": "^1.2.5",
"@lingui/core": "^6.8.0",
"@lingui/react": "^6.8.0",
@@ -199,7 +199,7 @@ export function ApplicationInsights({ applications }: { applications: Applicatio
const FLOW_COLORS = ["#a5b4fc", "#818cf8", "#22d3ee", "#fbbf24", "#34d399"];
const FLOW_BG = "#0a0a0f";
const FLOW_REJECTED = "#fb7185";
const FLOW_FONT = '"Hanken Grotesk Variable", "Hanken Grotesk", ui-sans-serif, sans-serif';
const FLOW_FONT = "var(--font-ui)";
// RxR mark ~18px wide in the 256-unit icon viewBox (the mark's glyphs span y ≈ 36–220).
const ICON_SCALE = 18 / 256;
@@ -211,10 +211,16 @@ function toBase64(buffer: ArrayBuffer): string {
}
// A rasterized SVG (loaded as an <img>) can't reach the page's webfonts, so the exported PNG falls
// back to a system font unless the font is inlined. Find the Hanken Grotesk woff2 the app already
// back to a system font unless the font is inlined. Find the UI woff2 the app already
// loaded (basic-latin subset covers the chart's English labels), base64 it, and return an
// @font-face the export SVG can embed. Returns null on any failure so export still proceeds.
async function uiFontFace(): Promise<string | null> {
const uiFamily = getComputedStyle(document.documentElement)
.getPropertyValue("--font-ui")
.split(",")[0]
?.trim()
.replace(/["']/g, "");
if (!uiFamily) return null;
for (const sheet of Array.from(document.styleSheets)) {
let rules: CSSRuleList | undefined;
try {
@@ -225,7 +231,7 @@ async function uiFontFace(): Promise<string | null> {
for (const rule of Array.from(rules ?? [])) {
if (!(rule instanceof CSSFontFaceRule)) continue;
const family = rule.style.getPropertyValue("font-family").replace(/["']/g, "");
if (!family.includes("Hanken Grotesk")) continue;
if (family !== uiFamily) continue;
if ((rule.style.getPropertyValue("font-style") || "normal") !== "normal") continue;
// Keep only the basic-latin subset (covers the chart's English labels). CSSOM normalizes
// its range to "U+0-FF" — i.e. "U+" then all-zero start — so match that, not "U+0000".
@@ -237,7 +243,7 @@ async function uiFontFace(): Promise<string | null> {
const res = await fetch(url);
if (!res.ok) return null;
const buffer = await res.arrayBuffer();
return `@font-face{font-family:"Hanken Grotesk Variable";font-style:normal;font-weight:100 900;src:url(data:font/woff2;base64,${toBase64(buffer)}) format("woff2");}`;
return `@font-face{font-family:"${family}";font-style:normal;font-weight:${rule.style.getPropertyValue("font-weight")};src:url(data:font/woff2;base64,${toBase64(buffer)}) format("woff2");}`;
} catch {
return null;
}
@@ -273,8 +279,9 @@ function PipelineFlow({ insights }: { insights: ReturnType<typeof computeInsight
if (!svg) return;
// Reveal the export-only watermark on a clone so the on-screen chart stays clean.
const clone = svg.cloneNode(true) as SVGSVGElement;
clone.style.fontFamily = getComputedStyle(svg).fontFamily;
for (const el of clone.querySelectorAll<SVGElement>("[data-export-only]")) el.style.display = "";
// Inline the UI font so the rasterized PNG renders in Hanken Grotesk, not a system fallback.
// Inline the UI font so the rasterized PNG uses the same type as the app.
const fontFace = await uiFontFace();
if (fontFace) {
const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
+2 -2
View File
@@ -126,8 +126,8 @@ export function LandingFooter() {
{/* It fades in by sliding up out of the mask's faded bottom; an opacity fade would fail contrast checks. */}
<div aria-hidden="true" className="@container mx-auto mt-14 max-w-[1440px] px-(--gutter)">
<div className="h-[23cqw] overflow-hidden [mask-image:linear-gradient(to_bottom,#000_35%,transparent_100%)]">
<div className="select-none whitespace-nowrap pb-[.06em] font-anybody font-extrabold font-stretch-[78%] text-[17.5cqw] text-ink leading-[.84] tracking-[-.035em] [transform:translateY(calc((1-clamp(0,(var(--p)-.1)/.8,1))*60%))] [transition:transform_.35s_var(--ease)]">
<div className="h-[27cqw] overflow-hidden [mask-image:linear-gradient(to_bottom,#000_35%,transparent_100%)]">
<div className="select-none whitespace-nowrap pt-[.12em] pb-[.06em] font-anybody font-extrabold font-stretch-[78%] text-[17.5cqw] text-ink leading-[.84] tracking-[-.035em] [transform:translateY(calc((1-clamp(0,(var(--p)-.1)/.8,1))*60%))] [transition:transform_.35s_var(--ease)]">
<span className="block">Reactive</span>
<span className="block text-accent-text">Resume</span>
</div>
+3 -5
View File
@@ -3,15 +3,13 @@
* section's progress as --p (0 to 1) and every motion below is derived from it with calc(), so React only re-renders
* on coarse steps. Imported by index.css, not the route, so a server-prerendered page is styled at first paint.
*/
@import "@fontsource-variable/anybody/wdth.css";
@import "@fontsource-variable/martian-mono/wdth.css";
/* Short viewports hide the secondary captions of the pinned scenes so nothing overlaps. */
@custom-variant short (@media (height < 720px));
@theme {
--font-anybody: "Anybody Variable", "Noto Sans", system-ui, sans-serif;
--font-martian: "Martian Mono Variable", ui-monospace, monospace;
/* Existing landing classes share the app's display and UI families. */
--font-anybody: var(--font-display);
--font-martian: var(--font-ui);
--animate-breathe: landing-breathe 16s ease-in-out infinite alternate;
--animate-drift: landing-drift 90s ease-in-out infinite alternate;
+1 -1
View File
@@ -24,7 +24,7 @@ export function Homepage() {
useScrollScenes(root);
return (
<div ref={root} className="landing relative isolate overflow-x-clip bg-bg font-display text-ink">
<div ref={root} className="landing relative isolate overflow-x-clip bg-bg font-ui text-ink">
<LandingBackground />
<a
href="#main"
+4 -4
View File
@@ -13,10 +13,10 @@ import { Icon } from "@reactive-resume/ui/components/icon";
*/
const fonts = {
sans: "'Hanken Grotesk Variable', system-ui, sans-serif",
serif: "'Newsreader Variable', Georgia, serif",
mono: "'Martian Mono Variable', ui-monospace, monospace",
display: "'Anybody Variable', system-ui, sans-serif",
sans: "var(--font-ui)",
serif: "var(--font-display)",
mono: "var(--font-mono)",
display: "var(--font-display)",
};
export type SheetTemplate = {
+2 -2
View File
@@ -5,7 +5,7 @@ import { Link } from "@tanstack/react-router";
import { Icon } from "@reactive-resume/ui/components/icon";
import { cn } from "@reactive-resume/utils/style";
/** A Martian Mono label: section kickers, scene numbers, small caps metadata. */
/** A UI label: section kickers, scene numbers, small caps metadata. */
export const labelClass =
"font-martian font-medium text-[11px] uppercase leading-[1.4] tracking-[.08em] font-stretch-[87.5%]";
@@ -39,7 +39,7 @@ type SceneCaptionProps = {
className?: string;
};
/** A pinned scene's caption: "01 / Write" over a sentence or two in Newsreader. */
/** A pinned scene's caption: "01 / Write" over a sentence or two in the display family. */
export function SceneCaption({ number, title, children, action, className }: SceneCaptionProps) {
return (
<div className={cn("flex flex-col gap-3", className)}>
-1
View File
@@ -20,7 +20,6 @@
"dependencies": {
"@base-ui/react": "^1.8.0",
"@fontsource-variable/hanken-grotesk": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource-variable/newsreader": "^5.3.0",
"@phosphor-icons/web": "^2.1.2",
"@reactive-resume/utils": "workspace:*",
+8 -4
View File
@@ -1,10 +1,10 @@
@import "tailwindcss" source(none);
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/hanken-grotesk";
@import "@fontsource-variable/hanken-grotesk/wght-italic.css";
@import "@fontsource-variable/newsreader/opsz.css";
@import "@fontsource-variable/newsreader/opsz-italic.css";
@import "@fontsource-variable/hanken-grotesk";
@import "@fontsource-variable/jetbrains-mono";
/* Phosphor stays for icons inside resumes (their names are stored in resume data). */
@import "@phosphor-icons/web/regular/style.css";
@@ -134,8 +134,8 @@
@theme {
--font-display: "Newsreader Variable", Georgia, serif;
--font-ui: "Hanken Grotesk Variable", system-ui, sans-serif;
--font-sans: "Hanken Grotesk Variable", system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
--font-sans: var(--font-ui);
--font-mono: ui-monospace, monospace;
/* 6 chips and small buttons · 8 controls · 10 list items · 12 cards and menus · 16 dialogs · 18 mobile sheets */
--radius-sm: 6px;
@@ -217,6 +217,10 @@
@apply bg-bg text-ink text-sm proportional-nums antialiased selection:bg-accent-soft selection:text-ink;
}
h1 {
@apply font-display;
}
/* A 2px accent ring with a 2px gap on every interactive element; never removed. */
:focus-visible {
outline: 2px solid var(--accent);
-24
View File
@@ -528,12 +528,6 @@ importers:
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@19.3.0)
'@fontsource-variable/anybody':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/martian-mono':
specifier: ^5.3.0
version: 5.3.0
'@lezer/highlight':
specifier: ^1.2.5
version: 1.2.5
@@ -1366,9 +1360,6 @@ importers:
'@fontsource-variable/hanken-grotesk':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/jetbrains-mono':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/newsreader':
specifier: ^5.3.0
version: 5.3.0
@@ -2752,18 +2743,9 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource-variable/anybody@5.3.0':
resolution: {integrity: sha512-3u/FSV3mGv9xW3x+CdK9lAZYT1Y3NaRA1bexeoIuk9s5QE8sD8OOTcofvpPx0Z6M0Fq1WAJ06ZAw8hivdRQWHQ==}
'@fontsource-variable/hanken-grotesk@5.3.0':
resolution: {integrity: sha512-DOd0FqL+3cH2dfNy0JtSq9tUfxBz5mLmHMjTU0DOr3E9Zo6wxuatCl53DIPNrSPgCsnuKyM739BTIG5glCLirw==}
'@fontsource-variable/jetbrains-mono@5.3.0':
resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
'@fontsource-variable/martian-mono@5.3.0':
resolution: {integrity: sha512-0wVODx9ILwNnaYC4XCuSaFRYtj1Prd89MRVaSuv2ng6zLiQkS4SepaWEPefs7vqCZS8sbEOA8jb/4F5KNMnGdg==}
'@fontsource-variable/newsreader@5.3.0':
resolution: {integrity: sha512-rrzYi43qMpbzwuFtf9OkWH8sxAPVPcQQQEwXpPtwaKYeJ8yVg5aLs5kawmo1f2Q1t1M38TLmEKCkGVDsYwgdFw==}
@@ -10129,14 +10111,8 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
'@fontsource-variable/anybody@5.3.0': {}
'@fontsource-variable/hanken-grotesk@5.3.0': {}
'@fontsource-variable/jetbrains-mono@5.3.0': {}
'@fontsource-variable/martian-mono@5.3.0': {}
'@fontsource-variable/newsreader@5.3.0': {}
'@formepdf/core@0.25.0(react@19.3.0)':