mirror of
https://github.com/soconnor0919/personal-website.git
synced 2025-12-11 06:14:44 -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",
|
||||
"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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
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() {
|
||||
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">
|
||||
© {new Date().getFullYear()} {name[0]?.first}
|
||||
|
||||
@@ -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'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">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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} ${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">
|
||||
© {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}
|
||||
</p>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
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 = {
|
||||
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?
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))",
|
||||
|
||||
Reference in New Issue
Block a user