diff --git a/src/components/ExternalLink.astro b/src/components/ExternalLink.astro index 53ae362d..6f3555b5 100644 --- a/src/components/ExternalLink.astro +++ b/src/components/ExternalLink.astro @@ -59,31 +59,16 @@ import {cn} from '../utils/cn'; * ``` */ export interface Props extends HTMLAttributes<'a'> { - /** External URL (required) */ href: string; - - /** Visual style variant */ variant?: 'text' | 'button' | 'button-outlined' | 'card-action' | 'badge'; - - /** Size variant */ size?: 'xs' | 'sm' | 'base' | 'lg'; - - /** Accessible label - automatically appends "(opens in new tab)" */ + /** Automatically appends "(opens in new tab)" */ ariaLabel: string; - - /** Optional icon prefix */ prefixIcon?: 'pdf' | 'github' | 'external' | 'none'; - - /** Show arrow suffix (default: true) */ showArrow?: boolean; - - /** Button color scheme (only for variant='button') */ + /** Only applies to variant='button' */ colorScheme?: 'accent' | 'neutral'; - - /** Additional classes via cn() utility */ class?: string; - - /** data-testid for testing */ 'data-testid'?: string; } diff --git a/src/components/Hero.astro b/src/components/Hero.astro index 5a1b500f..76f415dd 100644 --- a/src/components/Hero.astro +++ b/src/components/Hero.astro @@ -32,6 +32,10 @@ const {class: className, showImage = true, ...attrs} = Astro.props; // Profile data from centralized source const profile = HERO_PROFILE; + +// Animation timing constants (matches CSS sequence) +const ANIMATION_BASE_DELAY = 500; +const ANIMATION_STAGGER = 150; ---
{cred.label} diff --git a/src/components/ProjectImage.astro b/src/components/ProjectImage.astro index 3d6e0d0a..63455d06 100644 --- a/src/components/ProjectImage.astro +++ b/src/components/ProjectImage.astro @@ -21,13 +21,9 @@ import {cn} from '../utils/cn'; * ``` */ interface Props extends HTMLAttributes<'div'> { - /** Image metadata from content collection */ image?: ImageMetadata; - /** Alt text for the image */ alt: string; - /** Layout variant: 'featured' for 16:9 aspect ratio, 'thumbnail' for square */ variant: 'featured' | 'thumbnail'; - /** Whether to add hover scale effect (requires parent with group class) */ hoverScale?: boolean; } @@ -41,7 +37,6 @@ const { } = Astro.props; const hasValidImage = !!image; -const imageSource = image || placeholderImage; // Base classes for all images const baseImageClasses = 'w-full h-full object-cover'; @@ -49,6 +44,59 @@ const hoverClasses = hoverScale ? 'transition-transform duration-300 group-hover:scale-105' : ''; +// Image configuration based on variant and image availability +type ImageConfig = { + src: ImageMetadata; + widths?: number[]; + width?: number; + height?: number; + formats?: ('avif' | 'webp')[]; + class: string; +}; + +function getImageConfig( + hasImage: boolean, + imageVariant: 'featured' | 'thumbnail', + imageSource: ImageMetadata +): ImageConfig { + if (hasImage && imageVariant === 'featured') { + return { + src: imageSource, + widths: [400, 800, 1200], + formats: ['avif', 'webp'], + class: cn(baseImageClasses, hoverClasses), + }; + } + + if (hasImage && imageVariant === 'thumbnail') { + return { + src: imageSource, + width: 96, + height: 96, + class: cn(baseImageClasses, hoverClasses), + }; + } + + if (imageVariant === 'featured') { + return { + src: placeholderImage, + width: 800, + height: 450, + class: baseImageClasses, + }; + } + + // thumbnail placeholder + return { + src: placeholderImage, + width: 96, + height: 96, + class: baseImageClasses, + }; +} + +const imageConfig = getImageConfig(hasValidImage, variant, image || placeholderImage); + // Container classes based on variant const containerClasses = variant === 'featured' @@ -57,45 +105,14 @@ const containerClasses = ---
- { - hasValidImage ? ( - variant === 'featured' ? ( - {alt} - ) : ( - {alt} - ) - ) : variant === 'featured' ? ( - {alt} - ) : ( - {alt} - ) - } + {alt}
diff --git a/src/components/PublicationCard.astro b/src/components/PublicationCard.astro index 6230c5d1..449e4951 100644 --- a/src/components/PublicationCard.astro +++ b/src/components/PublicationCard.astro @@ -1,6 +1,6 @@ --- import type {HTMLAttributes} from 'astro/types'; -import {cardSurfaceContainerClasses} from '../utils/card-styles'; +import {cardSurfaceContainerClasses, BULLET_SEPARATOR} from '../utils/card-styles'; import {toSlugId} from '../utils/id-utils'; import ExternalLink from './ExternalLink.astro'; @@ -26,21 +26,14 @@ import ExternalLink from './ExternalLink.astro'; * ``` */ export interface Props extends HTMLAttributes<'article'> { - /** Publication title */ title: string; - /** List of authors */ authors: string[]; - /** Conference, journal, or venue name */ venue: string; - /** Publication year */ year: number; - /** Abstract text (optional - toggle hidden if not provided) */ + /** Toggle hidden if not provided */ abstract?: string; - /** URL to PDF document */ pdfUrl?: string; - /** URL to code repository */ codeUrl?: string; - /** DOI URL */ doiUrl?: string; } @@ -79,7 +72,7 @@ const hasLinks = pdfUrl || codeUrl || doiUrl; class="flex items-center gap-2 text-sm text-text-secondary dark:text-text-secondary-dark mb-2" > {year} - + {venue} diff --git a/src/components/SEO.astro b/src/components/SEO.astro index a8dc55c0..abafd89d 100644 --- a/src/components/SEO.astro +++ b/src/components/SEO.astro @@ -1,4 +1,6 @@ --- +import {PROFILE_DATA} from '../data/profile'; + /** * SEO component for managing page metadata, Open Graph, Twitter cards, and JSON-LD * Handles all SEO-related meta tags in one place @@ -17,17 +19,11 @@ * ``` */ interface Props { - /** Page title (will have " | Cris Benge" appended) */ title: string; - /** Meta description for SEO (max ~155 characters recommended) */ description: string; - /** OG image path (defaults to /og-default.png) */ image?: string; - /** Canonical URL (defaults to current page URL) */ canonical?: string; - /** Prevent indexing (for draft pages) */ noIndex?: boolean; - /** Include JSON-LD Person schema (home page only) */ includePersonSchema?: boolean; } @@ -40,9 +36,9 @@ const { includePersonSchema = false, } = Astro.props; -// Site configuration -const siteUrl = import.meta.env.SITE || 'https://cbenge509.github.io'; -const siteName = 'Cris Benge'; +// Site configuration from centralized profile data +const siteUrl = import.meta.env.SITE || PROFILE_DATA.siteUrl; +const siteName = PROFILE_DATA.name; // Computed values const fullTitle = `${title} | ${siteName}`; @@ -50,32 +46,22 @@ const ogImage = image.startsWith('http') ? image : `${siteUrl}${image}`; // Use site config + pathname for canonical URL (not Astro.url.href which uses localhost in dev) const canonicalUrl = canonical || `${siteUrl}${Astro.url.pathname}`; -// JSON-LD Person schema for the home page +// JSON-LD Person schema for the home page (built from centralized profile data) const personSchema = { '@context': 'https://schema.org', '@type': 'Person', - name: 'Cris Benge', - jobTitle: 'Head of Federal Innovation', + name: PROFILE_DATA.name, + jobTitle: PROFILE_DATA.jobTitle, worksFor: { '@type': 'Organization', - name: 'Google', + name: PROFILE_DATA.employer, }, url: siteUrl, - sameAs: [ - 'https://github.com/cbenge509', - 'https://www.linkedin.com/in/crisbenge/', - // Google Scholar URL to be added when available - ], - alumniOf: [ - { - '@type': 'CollegeOrUniversity', - name: 'Columbia University', - }, - { - '@type': 'CollegeOrUniversity', - name: 'UC Berkeley', - }, - ], + sameAs: PROFILE_DATA.socialProfiles, + alumniOf: PROFILE_DATA.education.map((edu) => ({ + '@type': 'CollegeOrUniversity', + name: edu.name, + })), }; --- diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index 2d51b25c..6159eab0 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -60,7 +60,8 @@ const {class: className, ...attrs} = Astro.props; const THEME_KEY = 'theme'; let memoryTheme: string | null = null; // Fallback for private browsing - function isStorageAvailable(): boolean { + // Cache localStorage availability (only check once) + const storageAvailable = (() => { try { const test = '__storage_test__'; localStorage.setItem(test, test); @@ -69,22 +70,18 @@ const {class: className, ...attrs} = Astro.props; } catch { return false; } - } + })(); function getTheme(): string | null { - if (isStorageAvailable()) { + if (storageAvailable) { return localStorage.getItem(THEME_KEY); } return memoryTheme; } function setTheme(theme: string): void { - try { - if (isStorageAvailable()) { - localStorage.setItem(THEME_KEY, theme); - } - } catch { - // Silently fail - use memory fallback + if (storageAvailable) { + localStorage.setItem(THEME_KEY, theme); } memoryTheme = theme; } diff --git a/src/data/profile.ts b/src/data/profile.ts index 04022825..5afa2461 100644 --- a/src/data/profile.ts +++ b/src/data/profile.ts @@ -57,3 +57,31 @@ export const HERO_PROFILE = { {label: 'TS/SCI w/ Polygraph', type: 'clearance'}, ] satisfies Credential[], } as const; + +/** + * Extended profile data for about page and SEO. + * Single source of truth for biographical and schema.org data. + */ +export const PROFILE_DATA = { + name: 'Cris Benge', + jobTitle: 'Head of Federal Innovation', + employer: 'Google', + siteUrl: 'https://cbenge509.github.io', + twitterHandle: '@cbaborern', + + bio: { + intro: + "I'm Cris Benge, Head of Federal Innovation at Google, where I lead AI/ML prototype initiatives that transform how U.S. federal agencies leverage cutting-edge technology to solve complex mission problems.", + experience: + 'With over two decades of experience in software engineering, data science, and technical leadership, I specialize in building scalable machine learning systems and driving innovation at the intersection of technology and public service.', + passion: + "I'm passionate about mentoring the next generation of technologists and contributing to open-source projects and research that advance the field of artificial intelligence.", + }, + + socialProfiles: [ + 'https://github.com/cbenge509', + 'https://www.linkedin.com/in/crisbenge/', + ], + + education: [{name: 'Columbia University'}, {name: 'UC Berkeley'}], +} as const; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index a58a3abe..5d7d23b9 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -10,6 +10,15 @@ import Footer from '../components/Footer.astro'; * Provides semantic HTML structure, SEO metadata, and accessibility features * @slot default - Page content (rendered inside
) * @slot head - Additional head content (rendered at end of ) + * + * NOTE: Pages using [data-reveal] elements must import scroll-reveal script: + * ```astro + * + * ``` + * This enables optimal code splitting - only pages that need animations load the script. + * * @example * ```astro * diff --git a/src/pages/about.astro b/src/pages/about.astro index 61795c2b..f141e8dd 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -9,6 +9,7 @@ import CertificationCard from '../components/CertificationCard.astro'; import AwardCard from '../components/AwardCard.astro'; import ContactSection from '../components/ContactSection.astro'; import {byOrder} from '../utils/collection-sort'; +import {PROFILE_DATA} from '../data/profile'; import profileImage from '../assets/images/profile/portfolio_image.jpg'; /** @@ -76,21 +77,13 @@ const hasAwards = sortedAwards.length > 0;

- I'm Cris Benge, Head of Federal Innovation at - Google, where I lead AI/ML prototype initiatives that transform how - U.S. federal agencies leverage cutting-edge technology to solve - complex mission problems. + {PROFILE_DATA.bio.intro}

- With over two decades of experience in software engineering, data - science, and technical leadership, I specialize in building scalable - machine learning systems and driving innovation at the intersection - of technology and public service. + {PROFILE_DATA.bio.experience}

- I'm passionate about mentoring the next generation of technologists - and contributing to open-source projects and research that advance - the field of artificial intelligence. + {PROFILE_DATA.bio.passion}

diff --git a/src/styles/global.css b/src/styles/global.css index 8b38716e..7f249b9e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -488,30 +488,9 @@ should also respect prefers-reduced-motion for smooth fallbacks. ============================================================================= */ -/* Transition utility classes with standard timing */ -.transition-fast { - transition-duration: var(--transition-fast); - transition-timing-function: var(--ease-out); -} - -.transition-medium { - transition-duration: var(--transition-medium); - transition-timing-function: var(--ease-out); -} - -.transition-slow { - transition-duration: var(--transition-slow); - transition-timing-function: var(--ease-out); -} - -/* Easing utility classes */ -.ease-out-custom { - transition-timing-function: var(--ease-out); -} - -.ease-in-out-custom { - transition-timing-function: var(--ease-in-out); -} +/* Note: Transition and easing utilities removed - using inline Tailwind classes instead. + Standard timing tokens (--transition-fast, --transition-medium, --transition-slow) + and easing tokens (--ease-out, --ease-in-out) remain available for direct use. */ /* ============================================================================= REDUCED MOTION SAFETY NET @@ -557,29 +536,9 @@ padding-right: var(--spacing-container-padding); } -/* Section spacing utility (use with Tailwind responsive prefixes) */ -.section-padding { - padding-top: var(--spacing-section-mobile); - padding-bottom: var(--spacing-section-mobile); -} - -@media (min-width: 768px) { - .section-padding { - padding-top: var(--spacing-section); - padding-bottom: var(--spacing-section); - } -} - -/* Card grid gap utility */ -.card-grid-gap { - gap: var(--spacing-card-gap-mobile); -} - -@media (min-width: 768px) { - .card-grid-gap { - gap: var(--spacing-card-gap); - } -} +/* Note: section-padding and card-grid-gap utilities removed - using inline Tailwind + responsive classes (py-16 md:py-24, gap-6 md:gap-8) instead. Spacing tokens + (--spacing-section, --spacing-card-gap) remain available for direct use. */ /* ============================================================================= 8. LINE CLAMP UTILITIES - Text Truncation diff --git a/src/utils/badge.ts b/src/utils/badge.ts index b50a8fd3..13ecaaa6 100644 --- a/src/utils/badge.ts +++ b/src/utils/badge.ts @@ -1,6 +1,10 @@ /** * Badge utility functions for consistent badge styling across card components. * Provides type-safe color mappings for common badge patterns. + * + * NOTE: Explicit color strings are intentionally used rather than a factory function. + * This design choice keeps colors readable, customizable, and compatible with + * Tailwind's JIT purging (dynamic class strings may not be detected). */ /** diff --git a/src/utils/card-styles.ts b/src/utils/card-styles.ts index d57a209d..75a25c23 100644 --- a/src/utils/card-styles.ts +++ b/src/utils/card-styles.ts @@ -3,6 +3,23 @@ import {cn} from './cn'; /** * Card styling utilities for consistent card component appearance. * Provides reusable class compositions for card containers and common patterns. + * + * CARD STYLE SELECTION GUIDE: + * + * cardContainerClasses(): + * - Interactive cards in grids + * - Cards that benefit from visual lift feedback + * - Example: EducationCard, CertificationCard + * + * cardSurfaceContainerClasses(): + * - Content-heavy cards where lift would distract + * - Cards with internal interactive elements + * - Example: PublicationCard, PatentCard + * + * interactiveCardClasses(): + * - Clickable cards with nested links + * - Cards needing focus-within support + * - Example: FeaturedProjectCard, SecondaryProjectCard */ /**