feat: Implement new 'soft, translucent, and alive' design system with updated UI components, navigation, and a new blog post.

This commit is contained in:
2025-12-10 03:08:08 -05:00
parent 347a61e1bf
commit 49243758c9
19 changed files with 507 additions and 319 deletions

View File

@@ -29,10 +29,10 @@
"fs": "0.0.1-security",
"geist": "^1.4.2",
"lucide-react": "^0.454.0",
"next": "^16.0.6",
"next": "16.0.6",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
"react": "^19.2.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-icons": "^5.5.0",
"react-pdf": "^9.2.1",

View File

@@ -49,12 +49,12 @@ export default async function BlogPost({ params }: PageProps) {
return (
<article className="animate-fade-in-up space-y-8">
<div className="mb-8">
<Button variant="ghost" asChild className="-ml-4 text-muted-foreground mb-4">
{/* <Button variant="ghost" asChild className="-ml-4 text-muted-foreground mb-4">
<Link href="/blog">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Blog
</Link>
</Button>
</Button> */}
<h1 className="text-3xl font-bold mb-4">{metadata.title}</h1>

View File

@@ -7,7 +7,7 @@ import { Navigation } from "~/components/Navigation";
import { Sidebar } from "~/components/Sidebar";
import { BreadcrumbWrapper } from "~/components/BreadcrumbWrapper";
import { inter } from "~/lib/fonts";
import { inter, playfair } from "~/lib/fonts";
import { description, name } from "~/lib/data";
import "~/styles/globals.css";
@@ -19,11 +19,21 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: React.PropsWithChildren) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<html
lang="en"
className={`${inter.variable} ${playfair.variable}`}
suppressHydrationWarning
>
<body
className="flex min-h-screen flex-col bg-background font-sans text-foreground"
className="flex min-h-screen flex-col bg-background font-sans text-foreground relative"
suppressHydrationWarning
>
{/* Background Elements */}
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none flex items-center justify-center">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
<div className="w-[800px] h-[800px] bg-neutral-400/40 dark:bg-neutral-500/30 rounded-full blur-3xl animate-blob"></div>
</div>
{env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
<Script
defer
@@ -36,20 +46,11 @@ export default function RootLayout({ children }: React.PropsWithChildren) {
)}
<Navigation />
<div className="flex flex-1">
{/* Desktop sidebar - extends to edge */}
<aside className="hidden overflow-y-auto lg:sticky lg:top-16 lg:block lg:h-[calc(100vh-4rem)]">
<Sidebar />
</aside>
<div className="flex-1">
{/* Mobile sidebar - horizontal intro bar only on homepage */}
<div className="px-4 sm:px-6 lg:hidden lg:px-8">
<Sidebar />
</div>
<div className="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<main className="py-8">
<div className="flex flex-1 pt-24 flex-col lg:flex-row">
<Sidebar />
<div className="flex-1 min-w-0 lg:pl-96">
<div className="mx-auto max-w-screen-xl px-6 sm:px-8 lg:pl-0 lg:pr-8">
<main className="pb-8 pt-4">
<BreadcrumbWrapper />
{children}
</main>

View File

@@ -68,7 +68,7 @@ export default function ProjectsPage() {
alt={project.imageAlt || project.title}
width={400}
height={300}
className="h-auto w-full object-contain"
className="h-auto w-full object-contain rounded-xl shadow-md"
priority={index === 0}
/>
</div>
@@ -114,7 +114,7 @@ export default function ProjectsPage() {
>
<Link href={project.link}>
{project.title ===
"LaTeX Introduction Tutorial" ? (
"LaTeX Introduction Tutorial" ? (
<>
<Play className="mr-2 h-4 w-4" />
Watch Tutorial
@@ -217,7 +217,7 @@ export default function ProjectsPage() {
alt={project.imageAlt || project.title}
width={400}
height={250}
className="h-auto max-h-full w-full object-contain"
className="h-auto max-h-full w-full object-contain rounded-xl shadow-md"
/>
</div>
)}

View File

@@ -54,7 +54,7 @@ export default function TripsPage() {
key={index}
className={`animate-fade-in-up-delay-${Math.min(index + 3, 4)} card-hover`}
>
<Card className="card-full-height overflow-hidden rounded-lg">
<Card className="card-full-height overflow-hidden">
<CardHeader className="p-0">
<div className="flex flex-col">
<div className="flex space-x-0 overflow-x-auto">

View File

@@ -0,0 +1,193 @@
"use client";
import { useState } from "react";
import { cn } from "~/lib/utils";
// Helper to convert HSL string (e.g., "0 0% 100%") to Hex
function hslToHex(hsl: string) {
const [h = 0, s = 0, l = 0] = hsl.split(" ").map((val) => parseFloat(val));
const hDecimal = h / 360;
const sDecimal = s / 100;
const lDecimal = l / 100;
let r, g, b;
if (s === 0) {
r = g = b = lDecimal; // achromatic
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = lDecimal < 0.5 ? lDecimal * (1 + sDecimal) : lDecimal + sDecimal - lDecimal * sDecimal;
const p = 2 * lDecimal - q;
r = hue2rgb(p, q, hDecimal + 1 / 3);
g = hue2rgb(p, q, hDecimal);
b = hue2rgb(p, q, hDecimal - 1 / 3);
}
const toHex = (x: number) => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
const THEME_COLORS = {
light: {
background: "0 0% 100%",
foreground: "240 10% 3.9%",
card: "0 0% 100%",
"card-foreground": "240 10% 3.9%",
popover: "0 0% 100%",
"popover-foreground": "240 10% 3.9%",
primary: "240 5.9% 10%",
"primary-foreground": "0 0% 98%",
secondary: "240 4.8% 90%",
"secondary-foreground": "240 5.9% 10%",
muted: "240 4.8% 95.9%",
"muted-foreground": "240 3.8% 46.1%",
accent: "240 4.8% 95.9%",
"accent-foreground": "240 5.9% 10%",
destructive: "0 84.2% 60.2%",
"destructive-foreground": "0 0% 98%",
border: "240 5.9% 90%",
input: "240 5.9% 90%",
ring: "240 10% 3.9%",
},
dark: {
background: "240 10% 3.9%",
foreground: "0 0% 98%",
card: "240 10% 3.9%",
"card-foreground": "0 0% 98%",
popover: "240 10% 3.9%",
"popover-foreground": "0 0% 98%",
primary: "0 0% 98%",
"primary-foreground": "240 5.9% 10%",
secondary: "240 3.7% 20%",
"secondary-foreground": "0 0% 98%",
muted: "240 3.7% 15.9%",
"muted-foreground": "240 5% 64.9%",
accent: "240 3.7% 15.9%",
"accent-foreground": "0 0% 98%",
destructive: "0 62.8% 30.6%",
"destructive-foreground": "0 0% 98%",
border: "240 3.7% 15.9%",
input: "240 3.7% 15.9%",
ring: "240 4.9% 83.9%",
},
};
const ColorSwatch = ({ hsl, title }: { hsl: string; title: string }) => {
const [copied, setCopied] = useState(false);
const hex = hslToHex(hsl);
const copyToClipboard = () => {
navigator.clipboard.writeText(hex);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
return (
<div className="group relative flex flex-col items-center">
<div
className={cn(
"h-12 w-12 flex-shrink-0 rounded-lg shadow-sm cursor-pointer transition-transform active:scale-95 border border-border/20",
)}
style={{ backgroundColor: hex }}
onClick={copyToClipboard}
title={`${title} (${hex})`}
/>
<div className="absolute -bottom-8 opacity-0 group-hover:opacity-100 transition-opacity bg-popover text-popover-foreground text-xs px-2 py-1 rounded shadow-md whitespace-nowrap z-10 pointer-events-none border border-border">
{copied ? "Copied!" : hex}
</div>
</div>
);
};
const ColorSection = ({ mode, colors }: { mode: "light" | "dark"; colors: typeof THEME_COLORS.light }) => {
return (
<div className="space-y-4">
<h4 className="font-heading font-bold capitalize text-lg">{mode} Mode</h4>
{/* Backgrounds */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<div className="w-24 text-sm font-medium">Backgrounds</div>
<div className="flex gap-2 flex-wrap">
<ColorSwatch hsl={colors.background} title="background" />
<ColorSwatch hsl={colors.card} title="card" />
<ColorSwatch hsl={colors.popover} title="popover" />
<ColorSwatch hsl={colors.muted} title="muted" />
<ColorSwatch hsl={colors.secondary} title="secondary" />
</div>
</div>
{/* Foregrounds */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<div className="w-24 text-sm font-medium">Foregrounds</div>
<div className="flex gap-2 flex-wrap">
<ColorSwatch hsl={colors.foreground} title="foreground" />
<ColorSwatch hsl={colors["card-foreground"]} title="card-foreground" />
<ColorSwatch hsl={colors["popover-foreground"]} title="popover-foreground" />
<ColorSwatch hsl={colors["muted-foreground"]} title="muted-foreground" />
<ColorSwatch hsl={colors["secondary-foreground"]} title="secondary-foreground" />
</div>
</div>
{/* Primary */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<div className="w-24 text-sm font-medium">Primary</div>
<div className="flex gap-2 flex-wrap">
<ColorSwatch hsl={colors.primary} title="primary" />
<ColorSwatch hsl={colors["primary-foreground"]} title="primary-foreground" />
</div>
</div>
{/* Destructive */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<div className="w-24 text-sm font-medium">Destructive</div>
<div className="flex gap-2 flex-wrap">
<ColorSwatch hsl={colors.destructive} title="destructive" />
<ColorSwatch hsl={colors["destructive-foreground"]} title="destructive-foreground" />
</div>
</div>
{/* UI Elements */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<div className="w-24 text-sm font-medium">UI Elements</div>
<div className="flex gap-2 flex-wrap">
<ColorSwatch hsl={colors.border} title="border" />
<ColorSwatch hsl={colors.input} title="input" />
<ColorSwatch hsl={colors.ring} title="ring" />
</div>
</div>
</div>
);
};
export function ColorPalette() {
return (
<div className="space-y-12 not-prose my-8">
<div>
<h3 className="text-lg font-bold mb-4">Scales</h3>
<p className="text-muted-foreground mb-6">
The system uses semantic color scales that adapt to the current theme. Hover to see hex codes.
</p>
<div className="grid gap-12 lg:grid-cols-2">
<ColorSection mode="light" colors={THEME_COLORS.light} />
<div className="h-px bg-border/50 lg:hidden" />
<ColorSection mode="dark" colors={THEME_COLORS.dark} />
</div>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { name } from "~/lib/data";
export function Footer() {
return (
<footer className="bg-background pb-4 text-foreground lg:hidden">
<footer className="bg-background py-2 text-foreground lg:hidden">
<div className="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<p className="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()} {name[0]?.first}&nbsp;

View File

@@ -34,13 +34,13 @@ export function Navigation() {
return (
<>
<nav
className={`sticky top-0 z-[51] border-b bg-card shadow-sm ${isOpen ? "border-transparent" : "border-border"
className={`fixed top-4 left-4 right-4 z-[51] rounded-2xl border bg-background/80 backdrop-blur-md shadow-sm transition-all duration-200 ${isOpen ? "border-transparent" : "border-border/60"
}`}
>
<div className="relative mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<div className="relative px-8">
<div className="flex h-16 items-center justify-between">
<Link href="/" className="flex items-center py-2">
<span className="text-xl font-semibold tracking-tight transition-colors hover:text-primary">
<span className="text-xl font-semibold font-heading tracking-tight transition-colors hover:text-primary">
Sean O&apos;Connor
</span>
</Link>
@@ -87,7 +87,7 @@ export function Navigation() {
aria-hidden="true"
/>
<div
className={`fixed left-0 right-0 top-16 z-50 overflow-hidden border-b border-border bg-card shadow-sm transition-all duration-300 lg:hidden ${isOpen ? "max-h-[calc(100vh-4rem)] opacity-100" : "max-h-0 opacity-0"
className={`fixed left-4 right-4 top-24 z-50 overflow-hidden rounded-2xl border border-border/50 bg-background/80 backdrop-blur-md shadow-sm transition-all duration-300 lg:hidden ${isOpen ? "max-h-[calc(100vh-8rem)] opacity-100" : "max-h-0 opacity-0"
}`}
>
<div className="flex flex-col space-y-2 p-4">

View File

@@ -12,6 +12,7 @@ import {
FileText,
Accessibility,
Briefcase,
File,
} from "lucide-react";
import {
Breadcrumb,
@@ -64,6 +65,10 @@ export function PageBreadcrumb() {
case "articles":
icon = <Newspaper className="mr-1 h-3.5 w-3.5" />;
break;
case "blog":
icon = <Newspaper className="mr-1 h-3.5 w-3.5" />;
label = "Blog";
break;
case "publications":
icon = <BookOpenText className="mr-1 h-3.5 w-3.5" />;
label = "Publications";
@@ -88,7 +93,7 @@ export function PageBreadcrumb() {
label = "Accessibility";
break;
default:
icon = <ChevronRight className="mr-1 h-3.5 w-3.5" />;
icon = <File className="mr-1 h-3.5 w-3.5" />;
}
breadcrumbItems.push({

View File

@@ -4,23 +4,30 @@ import Image from "next/image";
import { usePathname } from "next/navigation";
import { name, contact, location } from "~/lib/data";
import { useState } from "react";
import { Skeleton } from "~/components/ui/skeleton";
export function Sidebar() {
const pathname = usePathname();
const isHomePage = pathname === "/";
const [isImageLoading, setIsImageLoading] = useState(true);
return (
<>
{/* Mobile layout - horizontal intro bar only on home page */}
{isHomePage && (
<div className="w-full space-y-4 pb-2 pt-6 lg:hidden">
<div className="w-full space-y-4 px-6 pb-2 pt-6 sm:px-8 lg:hidden">
<div className="flex items-center space-x-4">
<div className="relative h-24 w-24 overflow-hidden">
<div className="relative h-24 w-24 overflow-hidden rounded-2xl border border-border/50 shadow-sm">
{isImageLoading && <Skeleton className="absolute inset-0 h-full w-full" />}
<Image
src="/headshot.png"
alt={`${name[0]?.first}&nbsp;${name[0]?.last}`}
width={240}
height={240}
className="object-cover"
className={`object-cover duration-700 ease-in-out ${isImageLoading ? "scale-110 blur-2xl grayscale" : "scale-100 blur-0 grayscale-0"
}`}
onLoad={() => setIsImageLoading(false)}
priority
/>
</div>
@@ -70,18 +77,21 @@ export function Sidebar() {
)}
{/* Desktop layout - clean and elegant sidebar */}
<div className="hidden h-full w-80 border-r border-border bg-card lg:block">
<div className="hidden fixed top-24 left-4 bottom-4 w-80 rounded-3xl border border-border/60 bg-background/80 backdrop-blur-xl lg:block overflow-hidden">
<div className="flex h-full flex-col">
{/* Profile Section */}
<div className="flex-shrink-0 border-b border-border px-8 py-8">
<div className="flex-shrink-0 border-b border-border/50 px-8 py-8">
<div className="flex flex-col items-center space-y-4">
<div className="relative h-40 w-40 overflow-hidden border border-border">
<div className="relative h-40 w-40 overflow-hidden border border-border/50 rounded-3xl shadow-sm">
{isImageLoading && <Skeleton className="absolute inset-0 h-full w-full" />}
<Image
src="/headshot.png"
alt={`${name[0]?.first} ${name[0]?.last}`}
width={400}
height={400}
className="h-full w-full object-cover"
className={`h-full w-full object-cover duration-700 ease-in-out ${isImageLoading ? "scale-110 blur-2xl grayscale" : "scale-100 blur-0 grayscale-0"
}`}
onLoad={() => setIsImageLoading(false)}
priority
/>
</div>
@@ -127,7 +137,7 @@ export function Sidebar() {
</div>
{/* Footer */}
<div className="flex-shrink-0 border-t border-border px-8 py-4">
<div className="flex-shrink-0 border-t border-border/50 px-8 py-4">
<p className="text-center text-xs text-muted-foreground">
&copy; {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}
</p>

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
@@ -22,9 +22,9 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "h-9 w-9 rounded-full",
},
},
defaultVariants: {
@@ -36,7 +36,7 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}

View File

@@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
"rounded-3xl border border-border/60 bg-background/80 backdrop-blur-xl text-card-foreground shadow-sm overflow-hidden",
className,
)}
{...props}

View File

@@ -50,7 +50,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden rounded-xl border border-border/50 bg-background/80 backdrop-blur-md p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
@@ -68,7 +68,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border border-border/50 bg-background/80 backdrop-blur-md p-1 text-popover-foreground shadow-md",
"origin-[--radix-dropdown-menu-content-transform-origin] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}

View File

@@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
"inline-flex h-9 items-center justify-center rounded-xl bg-background/80 backdrop-blur-md border border-border/50 shadow-sm p-1 text-muted-foreground",
className,
)}
{...props}

View File

@@ -0,0 +1,123 @@
export const metadata = {
title: "Designing My System: Soft, Translucent, and Alive",
publishedAt: "2025-12-10",
summary: "A deep dive into the design philosophy behind my personal website's new theme, moving from rigid boxes to organic, living layers.",
tags: ["Design", "UI/UX", "TailwindCSS", "Frontend"],
image: "/images/design-system.png"
};
## The Philosophy: Soft, Translucent, and Alive
When I set out to redesign my personal website, I wanted to move away from the standard "developer portfolio" aesthetic—rigid grids, harsh borders, and flat colors. I wanted something that felt organic, something that breathed.
I call this simply **My Design System**. It's not a product; it's a reflection of my personal aesthetic. The core philosophy rests on three pillars:
1. **Soft**: Replacing sharp corners with deep `1rem` (16px) to `1.5rem` (24px) border radii.
2. **Translucent**: Using glassmorphism to create depth without heavy drop shadows.
3. **Alive**: Incorporating subtle, continuous motion that makes the site feel like a living organism rather than a static document.
## The "Living Blob"
The heartbeat of this theme is the background. Instead of a flat color or a static gradient, I implemented what I call the "Living Blob".
```tsx
// src/app/layout.tsx
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none flex items-center justify-center">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px)...] bg-[size:24px_24px]"></div>
<div className="w-[800px] h-[800px] bg-neutral-400/40 dark:bg-neutral-500/30 rounded-full blur-3xl animate-blob"></div>
</div>
```
This single element—a massive, blurred circle—animates on a 7-second infinite loop, gently pulsing and shifting. It sits behind a technical grid pattern, creating a juxtaposition between the organic and the engineered.
## Floating UI
One of the biggest structural changes was detaching the navigation and sidebar from the viewport edges.
In a traditional layout, the sidebar is stuck to the left, and the navbar is stuck to the top. In this system, they are **floating cards**.
- **Navbar**: Fixed `top-4`, `left-4`, `right-4`.
- **Sidebar**: Fixed `top-24`, `left-4`, `bottom-4`.
This creates a sense of layering. The UI elements aren't *part* of the window; they are tools floating *above* the content.
## Design Tokens
### 1. Shape & Rounding
I chose a base radius of `1rem` (16px) because it strikes the perfect balance between friendly and professional.
- **Cards**: `rounded-3xl` (24px). Large containers need softer corners to feel less imposing.
- **Buttons**: `rounded-xl` (12px). Tactile and clickable.
- **Icons**: `rounded-full`.
This softness extends to interaction states. When you hover over a card, the `overflow-hidden` property ensures that any inner content (like images or background fills) respects these curves perfectly.
### 2. Shadows & Glassmorphism
Instead of heavy drop shadows to show depth, I rely on **Glassmorphism**.
- **Surface**: `bg-background/80` with `backdrop-blur-md`. This allows the "Living Blob" to bleed through, tinting the UI with the background color.
- **Border**: `border-border/60`. A subtle, semi-transparent border defines the edges.
- **Shadow**: `shadow-sm`. A very light lift, just enough to separate the layer.
### 3. Color Palette
The system uses a strict monochrome HSL palette that adapts to the user's system preference.
import { ColorPalette } from "~/components/ColorPalette";
<ColorPalette />
## Typography: Editorial Meets Digital
For typography, I wanted to blend the readability of a digital product with the elegance of an editorial magazine. This "Editorial meets Digital" philosophy relies on the interplay between two distinct typefaces.
### The Serif: Playfair Display
<p className="text-4xl font-heading mb-6">Playfair Display</p>
I chose **Playfair Display** for all headings (`h1``h6`). It's a transitional serif typeface with high contrast strokes and delicate hairlines.
* **Why Serif?** Serifs feel "human", "established", and "emotional". They break the sterile "tech" vibe common in developer portfolios.
* **Why Playfair?** Its high contrast makes it perfect for large display sizes. It commands attention and adds a layer of sophistication that a sans-serif simply cannot achieve.
### The Sans: Inter
<p className="text-4xl font-sans mb-6">Inter</p>
I chose **Inter** for the body text. It is the gold standard for screen readability.
* **Why Sans?** Sans-serif fonts are "rational", "clean", and "invisible". They reduce cognitive load, making long-form reading (like this blog post) effortless.
* **Why Inter?** It was designed specifically for computer screens, with a tall x-height that remains legible even at small sizes.
### Implementation
The font stack is implemented using `next/font` for zero layout shift and CSS variables for Tailwind integration.
```ts
// tailwind.config.ts
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
heading: ["var(--font-heading)", ...fontFamily.serif],
},
},
}
```
```css
/* src/styles/globals.css */
h1, h2, h3, h4, h5, h6 {
@apply font-heading; /* Playfair Display */
}
body {
@apply font-sans; /* Inter */
}
```
## Conclusion
This design system is more than just a theme; it's a statement about how I view software. It shouldn't just be functional; it should be inviting. It should feel like a space you want to inhabit. By combining soft shapes, organic motion, and editorial typography, I hope I've created a digital garden that feels a little more human.

View File

@@ -1,16 +1,11 @@
export const metadata = {
title: "Nand2Tetris Implementation (ECEG 431)",
title: "ECEG 431 E-Portfolio",
publishedAt: "2025-12-01",
summary: "E-Portfolio and reflection for the Nand2Tetris course, covering the journey from NAND gates to a high-level compiler.",
tags: ["Simulation", "Compilers", "Python", "Assembly", "VM", "OS"],
image: "/images/nand2tetris.png"
};
# ECEG 431 E-Portfolio
Sean O'Connor '26 \
Fall 2025
## Assignment Prompt
An end-of-course reflection activity that is both
@@ -24,7 +19,7 @@ Before starting this, take some time to read back through your reflections on gr
The story begins ... where it begins. A downtrodden situation? A rough situation? But something prompts you to seek a change of status.
> Aladdin goes about his life as usual, but then saves someone---and learns they are royalty! He then decides to set out to make something of his self (and maybe win a princess's heart).
> Aladdin goes about his life as usual, but then saves someone- and learns they are royalty! He then decides to set out to make something of his self (and maybe win a princess's heart).
What made you originally interested in this class? Or maybe, after hearing about things on the first week, what was it that really got you excited?

View File

@@ -1,6 +1,13 @@
import { Inter } from "next/font/google";
import { Inter, Playfair_Display } from "next/font/google";
export const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
});
export const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-heading",
display: "swap",
});

View File

@@ -4,199 +4,132 @@
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 0 0% 14.5%;
--card: 0 0% 94%;
--card-foreground: 0 0% 14.5%;
--popover: 0 0% 94%;
--popover-foreground: 0 0% 14.5%;
--primary: 0 0% 55.5%;
--primary-foreground: 0 0% 98.5%;
--secondary: 0 0% 92%;
--secondary-foreground: 0 0% 20.5%;
--muted: 0 0% 92%;
--muted-foreground: 0 0% 54.9%;
--accent: 0 0% 92%;
--accent-foreground: 0 0% 20.5%;
--destructive: 7 85% 58%;
--destructive-foreground: 0 0% 97%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 0 0% 71%;
--chart-1: 0 0% 55.5%;
--chart-2: 0 0% 55.5%;
--chart-3: 0 0% 55.5%;
--chart-4: 0 0% 55.5%;
--chart-5: 0 0% 55.5%;
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-sm:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
--shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
--shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--tracking-normal: 0em;
--spacing: 0.25rem;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
/* Darker secondary for better badge contrast against white bg */
--secondary: 240 4.8% 90%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-5: 27 87% 67%;
--radius: 1rem;
--sidebar: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
@media (prefers-color-scheme: dark) {
:root {
--background: 0 0% 12%;
--foreground: 0 0% 98.5%;
--card: 0 0% 18%;
--card-foreground: 0 0% 98.5%;
--popover: 0 0% 26.9%;
--popover-foreground: 0 0% 98.5%;
--primary: 0 0% 55.5%;
--primary-foreground: 0 0% 98.5%;
--secondary: 0 0% 26.9%;
--secondary-foreground: 0 0% 98.5%;
--muted: 0 0% 26.9%;
--muted-foreground: 0 0% 71%;
--accent: 0 0% 37.2%;
--accent-foreground: 0 0% 98.5%;
--destructive: 7 85% 70%;
--destructive-foreground: 0 0% 26.9%;
--border: 0 0% 25%;
--input: 0 0% 43.9%;
--ring: 0 0% 55.5%;
--chart-1: 0 0% 55.5%;
--chart-2: 0 0% 55.5%;
--chart-3: 0 0% 55.5%;
--chart-4: 0 0% 55.5%;
--chart-5: 0 0% 55.5%;
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-sm:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
--shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
--shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 20%;
/* Slightly lighter for visibility */
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
body {
letter-spacing: var(--tracking-normal);
}
}
.border {
border: 1px solid hsl(var(--border));
}
.shadow {
box-shadow: var(--shadow);
}
/* Optional: Add smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Optional: Improve text rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground font-sans;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-heading;
}
}
/* Remove all rounded corners for mono theme */
@layer components {
/* Cards */
.rounded-xl,
.rounded-lg,
.rounded-md,
.rounded-sm,
.rounded,
.rounded-full {
border-radius: 0 !important;
@layer utilities {
.animate-blob {
animation: blob 7s infinite;
}
/* Override specific component rounded classes */
[class*="rounded-"] {
border-radius: 0 !important;
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
}
/* Debug and ensure card backgrounds are visible */
@layer components {
.bg-card {
background-color: hsl(var(--card)) !important;
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
.bg-background {
background-color: hsl(var(--background)) !important;
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
/* Remove any image gradients/filters */
@layer components {
img {
filter: none !important;
backdrop-filter: none !important;
}
}
/* Enhanced sidebar styling */
@layer components {
/* Smooth transitions for sidebar links */
.sidebar-link {
transition: all 0.15s ease-in-out;
border-radius: 0;
}
.sidebar-link:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
/* Profile image subtle border */
.profile-image {
border: 1px solid hsl(var(--border));
transition: border-color 0.2s ease;
}
.profile-image:hover {
border-color: hsl(var(--primary));
}
/* Truncate text in sidebar contact links */
.sidebar-contact-link span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 180px;
}
/* Animation utilities */
.animate-fade-in {
@@ -223,36 +156,28 @@ body {
animation: fadeInUp 0.5s ease-in-out 0.4s backwards;
}
.hover-lift {
transition: transform 0.2s ease-in-out;
}
.hover-lift:hover {
transform: translateY(-2px);
}
/* Card hover effects */
.card-hover {
transition: all 0.2s ease-in-out;
transition: all 0.3s ease-out;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px hsl(var(--foreground) / 0.1);
transform: translateY(-4px);
box-shadow: 0 12px 24px -10px hsl(var(--foreground) / 0.1);
}
/* Button hover effects to match card interactions */
/* Button hover effects */
.button-hover {
transition: all 0.2s ease-in-out;
transition: all 0.2s ease-out;
}
.button-hover:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px hsl(var(--foreground) / 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px -4px hsl(var(--foreground) / 0.1);
}
/* Equal height cards in grid layouts */
.grid-equal-height > * {
.grid-equal-height>* {
height: 100%;
}
@@ -268,31 +193,8 @@ body {
flex-direction: column;
}
/* Ensure buttons align to bottom of cards */
.card-button-bottom {
margin-top: auto;
}
/* Card layout improvements for consistent button positioning */
.card-full-height {
display: flex;
flex-direction: column;
}
.card-full-height .card-header {
flex-shrink: 0;
}
.card-full-height .card-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-full-height .card-footer {
flex-shrink: 0;
margin-top: auto;
.card-content-stretch p:not(:last-child) {
margin-bottom: 1rem;
}
/* Text wrapping and overflow utilities */
@@ -303,18 +205,6 @@ body {
hyphens: auto;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Ensure flex items don't shrink below content size */
.flex-shrink-0 {
flex-shrink: 0 !important;
}
/* Better line height for readability */
.leading-relaxed {
line-height: 1.625;
}
@@ -322,51 +212,13 @@ body {
.leading-tight {
line-height: 1.25;
}
/* Prevent layout shifts with min-width */
.min-w-0 {
min-width: 0;
}
/* Consistent spacing for list items */
.list-spacing li + li {
margin-top: 0.75rem;
}
/* Button positioning in card layouts */
.mt-auto {
margin-top: auto;
}
/* Grid layout improvements */
.grid-equal-height {
display: grid;
align-items: stretch;
}
/* Enhanced card content distribution */
.card-with-button {
height: 100%;
display: flex;
flex-direction: column;
}
.card-with-button .card-body {
flex: 1;
display: flex;
flex-direction: column;
}
.card-with-button .card-actions {
margin-top: auto;
padding-top: 1rem;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
@@ -377,8 +229,9 @@ body {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}

View File

@@ -3,17 +3,18 @@ import { fontFamily } from "tailwindcss/defaultTheme";
export default {
darkMode: "media",
content: ["./src/**/*.tsx"],
content: ["./src/**/*.tsx", "./src/**/*.mdx"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)"],
unineue: ["var(--font-unineue)"],
sans: ["var(--font-sans)", ...fontFamily.sans],
heading: ["var(--font-heading)", ...fontFamily.sans],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
xl: "calc(var(--radius) + 4px)",
},
colors: {
background: "hsl(var(--background))",