From 0be2898f4f1d1a6d8656e08c17cafdb10287ef3c Mon Sep 17 00:00:00 2001 From: Kai Chen Date: Sun, 5 Jul 2026 20:17:19 -0700 Subject: [PATCH] feat: adapt homepage avatar size to intro column height Shrink the identity photo on desktop when greeting copy is short so social links keep breathing room below the portrait instead of sitting flush against a fixed square crop. Co-authored-by: Claude Co-authored-by: Cursor --- app/components/home-social-links.tsx | 2 +- app/components/identity-photo-column.tsx | 76 ++++++++++++++++++++++++ app/page.tsx | 16 +---- 3 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 app/components/identity-photo-column.tsx diff --git a/app/components/home-social-links.tsx b/app/components/home-social-links.tsx index 6ca0de8..1a0cb8f 100644 --- a/app/components/home-social-links.tsx +++ b/app/components/home-social-links.tsx @@ -83,7 +83,7 @@ export default function HomeSocialLinks() { `home-social-link home-social-link--${brand}`; return ( -
+
{SOCIAL_LINKS.map(({ href, label, tip, ariaLabel, brand, external, icon }) => ( {external ? ( diff --git a/app/components/identity-photo-column.tsx b/app/components/identity-photo-column.tsx new file mode 100644 index 0000000..a6f0a06 --- /dev/null +++ b/app/components/identity-photo-column.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useLayoutEffect, useRef, useState } from "react"; +import AvatarCard from "./avatar-card"; +import HomeSocialLinks from "./home-social-links"; +import { TocSection } from "./toc-section"; + +const MIN_AVATAR_PX = 120; +/** Space between avatar bottom and social row (matches `gap-4`). */ +const AVATAR_SOCIAL_GAP_PX = 16; +const MD_MEDIA = "(min-width: 768px)"; + +function measureAvatarPx(column: HTMLElement, social: HTMLElement): number { + const available = column.clientHeight - social.offsetHeight - AVATAR_SOCIAL_GAP_PX; + const width = column.clientWidth; + return Math.max(MIN_AVATAR_PX, Math.min(width, available)); +} + +export default function IdentityPhotoColumn() { + const columnRef = useRef(null); + const socialRef = useRef(null); + const [avatarPx, setAvatarPx] = useState(null); + + useLayoutEffect(() => { + const column = columnRef.current; + const social = socialRef.current; + if (!column || !social) return; + + const update = () => { + if (!window.matchMedia(MD_MEDIA).matches) { + setAvatarPx(null); + return; + } + setAvatarPx(measureAvatarPx(column, social)); + }; + + update(); + + const ro = new ResizeObserver(update); + ro.observe(column); + ro.observe(social); + + const mq = window.matchMedia(MD_MEDIA); + mq.addEventListener("change", update); + + return () => { + ro.disconnect(); + mq.removeEventListener("change", update); + }; + }, []); + + return ( +
+
+ +
+ + +
+ +
+
+ +
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 0d5e3d8..97d5934 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,9 +1,8 @@ -import AvatarCard from "./components/avatar-card"; import JumpText from "./components/jump-text"; import ListeningCard from "./components/listening-card"; import WeatherCard from "./components/weather-card"; import ProjectsSplit from "./components/oxford-dul/projects-split"; -import HomeSocialLinks from "./components/home-social-links"; +import IdentityPhotoColumn from "./components/identity-photo-column"; import { TocSection } from "./components/toc-section"; import { getPinnedProjects } from "./lib/github-pinned"; import { getBerkeleyWeather } from "./lib/weather"; @@ -131,18 +130,7 @@ export default async function Home() {
- {/* Photo + social */} -
-
- -
- - - - - -
-
+
{/* ── Layer 2: Status Cards ───────────────────────────────── */}