mirror of
https://github.com/soconnor0919/personal-website.git
synced 2025-12-12 23:04:43 -05:00
feat: Implement new 'soft, translucent, and alive' design system with updated UI components, navigation, and a new blog post.
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -29,10 +29,10 @@
|
|||||||
"fs": "0.0.1-security",
|
"fs": "0.0.1-security",
|
||||||
"geist": "^1.4.2",
|
"geist": "^1.4.2",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "^16.0.6",
|
"next": "16.0.6",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
"react": "^19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-pdf": "^9.2.1",
|
"react-pdf": "^9.2.1",
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ export default async function BlogPost({ params }: PageProps) {
|
|||||||
return (
|
return (
|
||||||
<article className="animate-fade-in-up space-y-8">
|
<article className="animate-fade-in-up space-y-8">
|
||||||
<div className="mb-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">
|
<Link href="/blog">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Blog
|
Back to Blog
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button> */}
|
||||||
|
|
||||||
<h1 className="text-3xl font-bold mb-4">{metadata.title}</h1>
|
<h1 className="text-3xl font-bold mb-4">{metadata.title}</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Navigation } from "~/components/Navigation";
|
|||||||
import { Sidebar } from "~/components/Sidebar";
|
import { Sidebar } from "~/components/Sidebar";
|
||||||
import { BreadcrumbWrapper } from "~/components/BreadcrumbWrapper";
|
import { BreadcrumbWrapper } from "~/components/BreadcrumbWrapper";
|
||||||
|
|
||||||
import { inter } from "~/lib/fonts";
|
import { inter, playfair } from "~/lib/fonts";
|
||||||
import { description, name } from "~/lib/data";
|
import { description, name } from "~/lib/data";
|
||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
@@ -19,11 +19,21 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: React.PropsWithChildren) {
|
export default function RootLayout({ children }: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={inter.className} suppressHydrationWarning>
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${inter.variable} ${playfair.variable}`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
<body
|
<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
|
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 && (
|
{env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||||
<Script
|
<Script
|
||||||
defer
|
defer
|
||||||
@@ -36,20 +46,11 @@ export default function RootLayout({ children }: React.PropsWithChildren) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1 pt-24 flex-col lg:flex-row">
|
||||||
{/* Desktop sidebar - extends to edge */}
|
<Sidebar />
|
||||||
<aside className="hidden overflow-y-auto lg:sticky lg:top-16 lg:block lg:h-[calc(100vh-4rem)]">
|
<div className="flex-1 min-w-0 lg:pl-96">
|
||||||
<Sidebar />
|
<div className="mx-auto max-w-screen-xl px-6 sm:px-8 lg:pl-0 lg:pr-8">
|
||||||
</aside>
|
<main className="pb-8 pt-4">
|
||||||
|
|
||||||
<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">
|
|
||||||
<BreadcrumbWrapper />
|
<BreadcrumbWrapper />
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function ProjectsPage() {
|
|||||||
alt={project.imageAlt || project.title}
|
alt={project.imageAlt || project.title}
|
||||||
width={400}
|
width={400}
|
||||||
height={300}
|
height={300}
|
||||||
className="h-auto w-full object-contain"
|
className="h-auto w-full object-contain rounded-xl shadow-md"
|
||||||
priority={index === 0}
|
priority={index === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@ export default function ProjectsPage() {
|
|||||||
>
|
>
|
||||||
<Link href={project.link}>
|
<Link href={project.link}>
|
||||||
{project.title ===
|
{project.title ===
|
||||||
"LaTeX Introduction Tutorial" ? (
|
"LaTeX Introduction Tutorial" ? (
|
||||||
<>
|
<>
|
||||||
<Play className="mr-2 h-4 w-4" />
|
<Play className="mr-2 h-4 w-4" />
|
||||||
Watch Tutorial
|
Watch Tutorial
|
||||||
@@ -217,7 +217,7 @@ export default function ProjectsPage() {
|
|||||||
alt={project.imageAlt || project.title}
|
alt={project.imageAlt || project.title}
|
||||||
width={400}
|
width={400}
|
||||||
height={250}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function TripsPage() {
|
|||||||
key={index}
|
key={index}
|
||||||
className={`animate-fade-in-up-delay-${Math.min(index + 3, 4)} card-hover`}
|
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">
|
<CardHeader className="p-0">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex space-x-0 overflow-x-auto">
|
<div className="flex space-x-0 overflow-x-auto">
|
||||||
|
|||||||
193
src/components/ColorPalette.tsx
Normal file
193
src/components/ColorPalette.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { name } from "~/lib/data";
|
|||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
return (
|
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">
|
<div className="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
© {new Date().getFullYear()} {name[0]?.first}
|
© {new Date().getFullYear()} {name[0]?.first}
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export function Navigation() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<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">
|
<div className="flex h-16 items-center justify-between">
|
||||||
<Link href="/" className="flex items-center py-2">
|
<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'Connor
|
Sean O'Connor
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -87,7 +87,7 @@ export function Navigation() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<div
|
<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">
|
<div className="flex flex-col space-y-2 p-4">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
Accessibility,
|
Accessibility,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
|
File,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@@ -64,6 +65,10 @@ export function PageBreadcrumb() {
|
|||||||
case "articles":
|
case "articles":
|
||||||
icon = <Newspaper className="mr-1 h-3.5 w-3.5" />;
|
icon = <Newspaper className="mr-1 h-3.5 w-3.5" />;
|
||||||
break;
|
break;
|
||||||
|
case "blog":
|
||||||
|
icon = <Newspaper className="mr-1 h-3.5 w-3.5" />;
|
||||||
|
label = "Blog";
|
||||||
|
break;
|
||||||
case "publications":
|
case "publications":
|
||||||
icon = <BookOpenText className="mr-1 h-3.5 w-3.5" />;
|
icon = <BookOpenText className="mr-1 h-3.5 w-3.5" />;
|
||||||
label = "Publications";
|
label = "Publications";
|
||||||
@@ -88,7 +93,7 @@ export function PageBreadcrumb() {
|
|||||||
label = "Accessibility";
|
label = "Accessibility";
|
||||||
break;
|
break;
|
||||||
default:
|
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({
|
breadcrumbItems.push({
|
||||||
|
|||||||
@@ -4,23 +4,30 @@ import Image from "next/image";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { name, contact, location } from "~/lib/data";
|
import { name, contact, location } from "~/lib/data";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isHomePage = pathname === "/";
|
const isHomePage = pathname === "/";
|
||||||
|
const [isImageLoading, setIsImageLoading] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile layout - horizontal intro bar only on home page */}
|
{/* Mobile layout - horizontal intro bar only on home page */}
|
||||||
{isHomePage && (
|
{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="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
|
<Image
|
||||||
src="/headshot.png"
|
src="/headshot.png"
|
||||||
alt={`${name[0]?.first} ${name[0]?.last}`}
|
alt={`${name[0]?.first} ${name[0]?.last}`}
|
||||||
width={240}
|
width={240}
|
||||||
height={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
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,18 +77,21 @@ export function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Desktop layout - clean and elegant 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">
|
<div className="flex h-full flex-col">
|
||||||
{/* Profile Section */}
|
{/* 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="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
|
<Image
|
||||||
src="/headshot.png"
|
src="/headshot.png"
|
||||||
alt={`${name[0]?.first} ${name[0]?.last}`}
|
alt={`${name[0]?.first} ${name[0]?.last}`}
|
||||||
width={400}
|
width={400}
|
||||||
height={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
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +137,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* 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">
|
<p className="text-center text-xs text-muted-foreground">
|
||||||
© {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}
|
© {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -22,9 +22,9 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 px-3 text-xs",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 px-8",
|
||||||
icon: "h-9 w-9",
|
icon: "h-9 w-9 rounded-full",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -36,7 +36,7 @@ const buttonVariants = cva(
|
|||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -68,7 +68,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
123
src/content/blog/designing-pdf2md.mdx
Normal file
123
src/content/blog/designing-pdf2md.mdx
Normal 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.
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Nand2Tetris Implementation (ECEG 431)",
|
title: "ECEG 431 E-Portfolio",
|
||||||
publishedAt: "2025-12-01",
|
publishedAt: "2025-12-01",
|
||||||
summary: "E-Portfolio and reflection for the Nand2Tetris course, covering the journey from NAND gates to a high-level compiler.",
|
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"],
|
tags: ["Simulation", "Compilers", "Python", "Assembly", "VM", "OS"],
|
||||||
image: "/images/nand2tetris.png"
|
image: "/images/nand2tetris.png"
|
||||||
};
|
};
|
||||||
|
|
||||||
# ECEG 431 E-Portfolio
|
|
||||||
|
|
||||||
Sean O'Connor '26 \
|
|
||||||
Fall 2025
|
|
||||||
|
|
||||||
## Assignment Prompt
|
## Assignment Prompt
|
||||||
|
|
||||||
An end-of-course reflection activity that is both
|
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.
|
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?
|
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?
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { Inter } from "next/font/google";
|
import { Inter, Playfair_Display } from "next/font/google";
|
||||||
|
|
||||||
export const inter = Inter({
|
export const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
variable: "--font-sans",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const playfair = Playfair_Display({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-heading",
|
||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,199 +4,132 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 98%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 14.5%;
|
--foreground: 240 10% 3.9%;
|
||||||
--card: 0 0% 94%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 0 0% 14.5%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
--popover: 0 0% 94%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 14.5%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
--primary: 0 0% 55.5%;
|
--primary: 240 5.9% 10%;
|
||||||
--primary-foreground: 0 0% 98.5%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 92%;
|
/* Darker secondary for better badge contrast against white bg */
|
||||||
--secondary-foreground: 0 0% 20.5%;
|
--secondary: 240 4.8% 90%;
|
||||||
--muted: 0 0% 92%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
--muted-foreground: 0 0% 54.9%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--accent: 0 0% 92%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
--accent-foreground: 0 0% 20.5%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--destructive: 7 85% 58%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
--destructive-foreground: 0 0% 97%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--border: 0 0% 88%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--input: 0 0% 88%;
|
--border: 240 5.9% 90%;
|
||||||
--ring: 0 0% 71%;
|
--input: 240 5.9% 90%;
|
||||||
--chart-1: 0 0% 55.5%;
|
--ring: 240 10% 3.9%;
|
||||||
--chart-2: 0 0% 55.5%;
|
--chart-1: 12 76% 61%;
|
||||||
--chart-3: 0 0% 55.5%;
|
--chart-2: 173 58% 39%;
|
||||||
--chart-4: 0 0% 55.5%;
|
--chart-3: 197 37% 24%;
|
||||||
--chart-5: 0 0% 55.5%;
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 1rem;
|
||||||
--font-sans: Geist Mono, monospace;
|
--sidebar: 0 0% 98%;
|
||||||
--font-serif: Geist Mono, monospace;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--font-mono: Geist Mono, monospace;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
--radius: 0rem;
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--shadow-sm:
|
--sidebar-border: 220 13% 91%;
|
||||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
--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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 12%;
|
--background: 240 10% 3.9%;
|
||||||
--foreground: 0 0% 98.5%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 18%;
|
--card: 240 10% 3.9%;
|
||||||
--card-foreground: 0 0% 98.5%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 26.9%;
|
--popover: 240 10% 3.9%;
|
||||||
--popover-foreground: 0 0% 98.5%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 55.5%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 0 0% 98.5%;
|
--primary-foreground: 240 5.9% 10%;
|
||||||
--secondary: 0 0% 26.9%;
|
--secondary: 240 3.7% 20%;
|
||||||
--secondary-foreground: 0 0% 98.5%;
|
/* Slightly lighter for visibility */
|
||||||
--muted: 0 0% 26.9%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted-foreground: 0 0% 71%;
|
--muted: 240 3.7% 15.9%;
|
||||||
--accent: 0 0% 37.2%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
--accent-foreground: 0 0% 98.5%;
|
--accent: 240 3.7% 15.9%;
|
||||||
--destructive: 7 85% 70%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive-foreground: 0 0% 26.9%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--border: 0 0% 25%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--input: 0 0% 43.9%;
|
--border: 240 3.7% 15.9%;
|
||||||
--ring: 0 0% 55.5%;
|
--input: 240 3.7% 15.9%;
|
||||||
--chart-1: 0 0% 55.5%;
|
--ring: 240 4.9% 83.9%;
|
||||||
--chart-2: 0 0% 55.5%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-3: 0 0% 55.5%;
|
--chart-2: 160 60% 45%;
|
||||||
--chart-4: 0 0% 55.5%;
|
--chart-3: 30 80% 55%;
|
||||||
--chart-5: 0 0% 55.5%;
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
--font-sans: Geist Mono, monospace;
|
--sidebar: 240 5.9% 10%;
|
||||||
--font-serif: Geist Mono, monospace;
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
--font-mono: Geist Mono, monospace;
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
--radius: 0rem;
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
--shadow-sm:
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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 utilities {
|
||||||
@layer components {
|
.animate-blob {
|
||||||
/* Cards */
|
animation: blob 7s infinite;
|
||||||
.rounded-xl,
|
|
||||||
.rounded-lg,
|
|
||||||
.rounded-md,
|
|
||||||
.rounded-sm,
|
|
||||||
.rounded,
|
|
||||||
.rounded-full {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override specific component rounded classes */
|
.animation-delay-2000 {
|
||||||
[class*="rounded-"] {
|
animation-delay: 2s;
|
||||||
border-radius: 0 !important;
|
}
|
||||||
|
|
||||||
|
.animation-delay-4000 {
|
||||||
|
animation-delay: 4s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Debug and ensure card backgrounds are visible */
|
@keyframes blob {
|
||||||
@layer components {
|
0% {
|
||||||
.bg-card {
|
transform: translate(0px, 0px) scale(1);
|
||||||
background-color: hsl(var(--card)) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-background {
|
33% {
|
||||||
background-color: hsl(var(--background)) !important;
|
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 {
|
@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 */
|
/* Animation utilities */
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
@@ -223,36 +156,28 @@ body {
|
|||||||
animation: fadeInUp 0.5s ease-in-out 0.4s backwards;
|
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 effects */
|
||||||
.card-hover {
|
.card-hover {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-hover:hover {
|
.card-hover:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 4px 8px hsl(var(--foreground) / 0.1);
|
box-shadow: 0 12px 24px -10px hsl(var(--foreground) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button hover effects to match card interactions */
|
/* Button hover effects */
|
||||||
.button-hover {
|
.button-hover {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-hover:hover {
|
.button-hover:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 2px 4px hsl(var(--foreground) / 0.1);
|
box-shadow: 0 4px 12px -4px hsl(var(--foreground) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Equal height cards in grid layouts */
|
/* Equal height cards in grid layouts */
|
||||||
.grid-equal-height > * {
|
.grid-equal-height>* {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,31 +193,8 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure buttons align to bottom of cards */
|
.card-content-stretch p:not(:last-child) {
|
||||||
.card-button-bottom {
|
margin-bottom: 1rem;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text wrapping and overflow utilities */
|
/* Text wrapping and overflow utilities */
|
||||||
@@ -303,18 +205,6 @@ body {
|
|||||||
hyphens: auto;
|
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 {
|
.leading-relaxed {
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
}
|
}
|
||||||
@@ -322,51 +212,13 @@ body {
|
|||||||
.leading-tight {
|
.leading-tight {
|
||||||
line-height: 1.25;
|
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 {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -377,8 +229,9 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,18 @@ import { fontFamily } from "tailwindcss/defaultTheme";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: "media",
|
darkMode: "media",
|
||||||
content: ["./src/**/*.tsx"],
|
content: ["./src/**/*.tsx", "./src/**/*.mdx"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-sans)"],
|
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||||
unineue: ["var(--font-unineue)"],
|
heading: ["var(--font-heading)", ...fontFamily.sans],
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
xl: "calc(var(--radius) + 4px)",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
|
|||||||
Reference in New Issue
Block a user