Skip to content

Commit 87359a1

Browse files
author
Hem Gabhawala
committed
feat: redesign to Design System v2 — flat, high-contrast, editorial
Replace the glassmorphism + glow aesthetic with a flat, high-contrast editorial system per Design System Reference v2. Confidence now comes from typography scale and whitespace; the accent is a functional signal (CTAs, active states, key stats, links), never a background flood. No blur, no glow, no gradient washes. Foundation - styles/globals.css: introduce v2 raw palette (--bg-base, --bg-surface, --text-primary/secondary/muted, --border-subtle, --accent #F57A28, --accent-hover #FF8C3F) and alias it into the existing semantic tokens so every component consuming Tailwind classes inherits v2 for free. Expose the raw palette directly (bg-surface, text-muted, accent-hover, …). Add motion tokens (--duration-*, --ease-default) and oversized display type steps (text-7xl/8xl). Fix a stale .dark block that reverted tokens to the old teal/blue oklch palette for OS-dark visitors. - Remove all decoration: floating glow orbs + float keyframe, gradient-text fill (now a flat alias), glow-primary, and the old pill/floating-button glow utilities. Redefine the `glass` utility as a flat opaque surface (no blur), which flattens the header and footer automatically. - lib/motion.ts: adopt the v2 ease-default cubic-bezier and 24px reveal offset so every scroll reveal inherits v2 timing. - hooks/use-cursor.ts: flat light dot (mix-blend difference), grows and turns accent on interactive elements, no glow. Sections - Hero: flat rebuild — oversized mixed-weight display headline with a single italic accent phrase, mono terminal role line, three stat counters on hairline dividers, flat accent + outline CTAs; line-by-line reveal. Removes grid background, glow layers, blur, and gradient text. - section-premium: flat backgrounds and hairline dividers only; headings are flat high-contrast with an optional italic accent word and numbered-marker support. - Sequential section numbering (01–09) with italic accent titles across About, Journey, Skills, Experience, Projects, Certifications, TryHackMe, GitHub, and Contact. New v2 component patterns - NumberedDetailList: numbered vertical list with an active-item detail panel (sticky on desktop, accordion on mobile). Journey and Experience now map their data into it, replacing the old marker/spine timeline. - ProjectRows: large clickable project rows with a cursor-following hover-reveal thumbnail (placeholder when no cover image); opens the existing project dialog. Replaces the project card grid. - Certifications, TryHackMe, and GitHub now render a shared flat "credential wall" — a hairline grid of cells where the issuer/owner reads as a label and the title turns accent on hover. GitHubCard drops shadcn Card/Button. - ScrollProgress: thin accent scroll-progress bar (spring-eased useScroll) fixed above the sticky nav; aria-hidden and reduced-motion aware. Cleanup - Remove superseded files: timeline-item, timeline-list, experience-item, experience-list, project-card, project-grid. No tests referenced them; tsc, ESLint, and a production build all pass.
1 parent 5599d6b commit 87359a1

30 files changed

Lines changed: 882 additions & 981 deletions

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
33
import { Footer } from "@/components/layout/footer";
44
import { SiteHeader } from "@/components/layout/site-header";
55
import { PageTransition } from "@/components/shared/page-transition";
6+
import { ScrollProgress } from "@/components/layout/scroll-progress";
67
import { MotionProvider } from "@/components/providers/motion-provider";
78
import { ThemeProvider } from "@/components/providers/theme-provider";
89
import { LayoutClient } from "@/components/layout/layout-client";
@@ -82,6 +83,7 @@ export default function RootLayout({
8283
<ThemeProvider>
8384
<MotionProvider>
8485
<LayoutClient>
86+
<ScrollProgress />
8587
<PageTransition />
8688
<SiteHeader />
8789
{children}
Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { ExternalLink } from "lucide-react";
1+
import { ArrowUpRight } from "lucide-react";
22

3-
import { Button } from "@/components/ui/button";
4-
import { Card, CardContent, CardHeader } from "@/components/ui/card";
53
import type { Certificate } from "@/lib/types";
4+
import { cn } from "@/lib/utils";
65

76
/**
8-
* One certification or achievement card. Server Component — everything is
9-
* renderable without client JS.
7+
* One credential cell in the logo/name grid (Design System v2 §6). Flat and
8+
* editorial: the issuer reads as a small tracked-out label (the "logo"), the
9+
* title is the primary type and turns accent on hover. When a verification URL
10+
* exists the whole cell is a link; otherwise it's a static cell.
1011
*
11-
* Used by both Certifications and TryHackMe sections (same Card component,
12-
* passed in as-is without branching logic), so properties stay generic and
13-
* the issuer/date metadata handles both credential types identically.
12+
* Server Component — renderable without client JS. Shared by both the
13+
* Certifications and TryHackMe sections without branching logic.
1414
*/
1515
export function CertificationCard({ cert }: { cert: Certificate }) {
1616
const { title, issuer, date, credentialUrl } = cert;
@@ -19,32 +19,47 @@ export function CertificationCard({ cert }: { cert: Certificate }) {
1919
month: "short",
2020
});
2121

22-
return (
23-
<Card className="h-full py-4 sm:py-6">
24-
<CardHeader className="pb-3 sm:pb-4">
25-
<h3 className="text-base leading-snug font-semibold tracking-tight sm:text-lg">
22+
const cls =
23+
"group flex h-full flex-col justify-between gap-6 bg-bg-base p-6 transition-colors duration-200 hover:bg-bg-surface sm:p-8";
24+
25+
const inner = (
26+
<>
27+
<div className="flex items-start justify-between gap-3">
28+
<span className="font-mono text-[11px] font-medium tracking-[0.12em] text-text-muted uppercase">
29+
{issuer}
30+
</span>
31+
{credentialUrl && (
32+
<ArrowUpRight
33+
aria-hidden
34+
className="size-4 shrink-0 text-text-muted opacity-0 transition-all duration-200 group-hover:text-accent group-hover:opacity-100"
35+
/>
36+
)}
37+
</div>
38+
<div>
39+
<h3
40+
className={cn(
41+
"text-base leading-snug font-semibold tracking-tight text-text-primary transition-colors duration-200 sm:text-lg",
42+
credentialUrl && "group-hover:text-accent",
43+
)}
44+
>
2645
{title}
2746
</h3>
28-
<p className="text-xs text-muted-foreground sm:text-sm">
29-
{issuer}{formattedDate}
30-
</p>
31-
</CardHeader>
47+
<p className="mt-2 text-xs text-text-muted">{formattedDate}</p>
48+
</div>
49+
</>
50+
);
3251

33-
{credentialUrl && (
34-
<CardContent className="pt-0">
35-
<Button asChild variant="ghost" size="sm">
36-
<a
37-
href={credentialUrl}
38-
target="_blank"
39-
rel="noopener noreferrer"
40-
aria-label={`View credential for ${title} (opens in a new tab)`}
41-
>
42-
<ExternalLink aria-hidden className="size-3.5" />
43-
View credential
44-
</a>
45-
</Button>
46-
</CardContent>
47-
)}
48-
</Card>
52+
return credentialUrl ? (
53+
<a
54+
href={credentialUrl}
55+
target="_blank"
56+
rel="noopener noreferrer"
57+
aria-label={`View credential for ${title} (opens in a new tab)`}
58+
className={cls}
59+
>
60+
{inner}
61+
</a>
62+
) : (
63+
<div className={cls}>{inner}</div>
4964
);
5065
}

components/certifications/certification-grid.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,38 @@
33
import { motion } from "motion/react";
44

55
import { CertificationCard } from "@/components/certifications/certification-card";
6-
import { fadeUp, staggerChildren, subtleHover } from "@/lib/motion";
6+
import { fadeIn, staggerChildren } from "@/lib/motion";
77
import type { Certificate } from "@/lib/types";
88

99
/**
10-
* Responsive card grid with staggered reveal. Server component children
11-
* (CertificationCard) are passed through motion wrappers per
12-
* COMPONENT_ARCHITECTURE.md — variants only propagate through motion
13-
* components, so the grid parent and each card wrapper are client, while
14-
* the card itself stays server-renderable.
10+
* Flat logo/name grid (Design System v2 §6 — the "enterprise partners" wall).
11+
* Cells are separated by hairline borders (no cards, no shadow, no gap-fill
12+
* blocks on partial rows): a top/left frame on the list plus right/bottom
13+
* borders per cell. Opacity-only stagger so the hairline seams stay stable
14+
* during the reveal.
1515
*
16-
* Used by both Certifications and TryHackMe sections so they share the
17-
* exact same grid layout, spacing, and reveal behavior.
16+
* Shared by Certifications and TryHackMe so both render the same credential
17+
* wall. Variants only propagate through motion components, hence the client
18+
* boundary here while each CertificationCard stays server-renderable.
1819
*/
1920
export function CertificationGrid({ certs }: { certs: Certificate[] }) {
2021
return (
21-
<motion.div
22+
<motion.ul
2223
initial="hidden"
2324
whileInView="visible"
2425
viewport={{ once: true, margin: "-80px" }}
2526
variants={staggerChildren}
26-
className="grid gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3"
27+
className="grid grid-cols-1 border-t border-l border-border-subtle sm:grid-cols-2 lg:grid-cols-3"
2728
>
2829
{certs.map((cert) => (
29-
<motion.div key={cert.id} variants={fadeUp} whileHover={subtleHover}>
30+
<motion.li
31+
key={cert.id}
32+
variants={fadeIn}
33+
className="border-r border-b border-border-subtle"
34+
>
3035
<CertificationCard cert={cert} />
31-
</motion.div>
36+
</motion.li>
3237
))}
33-
</motion.div>
38+
</motion.ul>
3439
);
3540
}

components/certifications/certifications.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export function Certifications() {
1212
<SectionPremium id="certifications" spacing="normal" accentLine="top">
1313
<SectionHeader
1414
eyebrow="Credentials"
15-
title="Certifications & Learning"
15+
index="06"
16+
title="Certifications &"
17+
titleAccent="learning"
1618
subtitle="Professional credentials and continuous education"
1719
/>
1820
<CertificationGrid certs={certs} />

components/certifications/tryhackme-section.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export function TryHackMeSection() {
1212
<SectionPremium id="tryhackme" spacing="normal" accentLine="top">
1313
<SectionHeader
1414
eyebrow="TryHackMe"
15-
title="Learning Paths & Rooms"
15+
index="07"
16+
title="Learning paths &"
17+
titleAccent="rooms"
1618
subtitle="Hands-on security challenges and practice"
1719
/>
1820
<CertificationGrid certs={achievements} />

components/contact/contact-section.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export function ContactSection() {
99
<SectionPremium id="contact" spacing="spacious" accentLine="top">
1010
<SectionHeader
1111
eyebrow="Contact"
12-
title="Let's work together"
12+
index="09"
13+
title="Let's work"
14+
titleAccent="together"
1315
subtitle="Have a project or opportunity? Get in touch"
1416
maxWidth="md"
1517
/>

components/experience/experience-item.tsx

Lines changed: 0 additions & 91 deletions
This file was deleted.

components/experience/experience-list.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

components/experience/experience-timeline.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,50 @@ import {
22
SectionPremium,
33
SectionHeader,
44
} from "@/components/layout/section-premium";
5-
import { ExperienceList } from "@/components/experience/experience-list";
5+
import {
6+
NumberedDetailList,
7+
type DetailListItem,
8+
} from "@/components/shared/numbered-detail-list";
69
import { getExperience } from "@/lib/data";
710

811
export function ExperienceTimeline() {
912
const experiences = getExperience();
1013

14+
const items: DetailListItem[] = experiences.map((exp) => {
15+
const isOngoing = exp.endDate === null;
16+
const end = isOngoing ? "Present" : formatMonthYear(exp.endDate as string);
17+
return {
18+
id: `${exp.company}-${exp.startDate}`,
19+
title: exp.role,
20+
meta: `${exp.company} · ${formatMonthYear(exp.startDate)}${end}`,
21+
tag: isOngoing ? "Present" : exp.company,
22+
tagHighlight: isOngoing,
23+
bullets: exp.achievements,
24+
};
25+
});
26+
1127
return (
1228
<SectionPremium id="experience" spacing="normal" accentLine="top">
1329
<SectionHeader
1430
eyebrow="Experience"
15-
title="Practical Security Work"
31+
index="04"
32+
title="Practical security"
33+
titleAccent="work"
1634
subtitle="Professional roles and hands-on security experience"
1735
/>
18-
<ExperienceList experiences={experiences} />
36+
<NumberedDetailList items={items} ariaLabel="Work experience" />
1937
</SectionPremium>
2038
);
2139
}
40+
41+
/** `YYYY-MM` → "May 2025" */
42+
function formatMonthYear(date: string): string {
43+
const [year, month] = date.split("-").map(Number);
44+
if (!year || !month) return date;
45+
const formatted = new Date(Date.UTC(year, month - 1, 1));
46+
return formatted.toLocaleDateString("en-US", {
47+
year: "numeric",
48+
month: "short",
49+
timeZone: "UTC",
50+
});
51+
}

0 commit comments

Comments
 (0)