Single-page portfolio for Ryan Wetzstein, built with React 19 and Vite, deployed to GitHub Pages at ryanwetzstein.com.
- Hero: name, availability line, primary action, and three decorative WebGL shader panels (desktop only)
- Projects: Nexis as a full-width featured card with a screenshot, then four supporting projects in a 2×2 grid
- About: bio beside a photo that stays in view (sticky) on wide screens
- Experience: sticky heading beside a timeline of education, internship, and capstone, kept in step with the resume
- Skills: one row per group with years on each chip; hover or keyboard focus shows a short note
- Contact: availability, email / GitHub / LinkedIn rows, and a validated form that opens the visitor's mail client
#nexis redirects to nexisdev.org, where Nexis now lives.
| Category | Library |
|---|---|
| Framework | React 19 + Vite 8 |
| Styling | Tailwind CSS v4 plus design tokens and layout classes in src/index.css |
| UI primitives | shadcn/ui on radix-ui (command, dialog, tooltip, sonner are in use) |
| Command palette | cmdk |
| Toasts | sonner |
| Animation | Framer Motion (LazyMotion + m), CSS transitions/keyframes |
| Shaders | Hand-written WebGL2 fragment shaders (src/lib/shaders.js) |
| Icons | @phosphor-icons/react (one family, including the shadcn primitives), Devicon for skill logos (CDN, pinned with SRI) |
| Fonts | Geist Variable and Geist Mono Variable, self-hosted via @fontsource-variable/* |
├── public/
│ ├── 404.html / 404.js # GitHub Pages 404 that echoes the missed path
│ ├── og-image.png # 1200×630 social preview
│ ├── favicon.svg # RW tile
│ ├── manifest.json
│ └── resume.pdf
├── src/
│ ├── assets/ # WebP images (profile, Nexis logo + screenshot), signature
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── Hero.jsx, Projects.jsx, About.jsx, Experience.jsx, Skills.jsx, Contact.jsx
│ │ ├── Navbar.jsx # scroll-spy + mobile menu
│ │ ├── PillNav.jsx # desktop nav capsule (CSS circle-wipe)
│ │ ├── ShaderPanel.jsx # WebGL2 canvas, pauses off-screen, still frame under reduced motion
│ │ ├── CommandPaletteHost.jsx # owns ⌘K state, lazy-loads CommandPalette.jsx
│ │ └── CommandPalette.jsx
│ ├── lib/
│ │ ├── motion-features.js # framer-motion features, loaded in their own chunk
│ │ ├── shaders.js
│ │ └── utils.ts # cn() helper
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
└── index.html # CSP, meta/OG tags, skip link
- Layout: every section's content sits in
.page-container(1200px, centred, 24px gutters). Sections own vertical padding only. - Rhythm: sections use
.section(fluid padding, slightly more below than above). - Type: Geist for text, Geist Mono for labels. Section headers use
.section-eyebrow(with a numbered.section-index),.section-title,.section-lede; small labels use.micro-label. Sentence case throughout, no all-caps. Long prose uses.prose-measure(65ch). Nothing renders below 12px. - Colour: one accent,
--m-accent(#a94a26, rust), used sparingly: section numbers, the status dot, link hovers, and the hero shader ramp. Text greys must pass WCAG AA:--m-subtle(#6b7079) is the lightest text colour;--m-border-strongis for borders only. The page stays in one light tone, with no dark bands. - Actions:
.btn-primaryfor the main action in a group,.text-linkfor the rest; avoid filled + ghost button pairs. Buttons lift on hover and scale to 0.98 on press. - Surfaces: cards use tinted elevation (
--m-shadow-sm/--m-shadow-lg) rather than borders. Keep z-index on the--z-*scale. - Motion: use
m.*components, notmotion.*(LazyMotionruns in strict mode and will throw).MotionConfig reducedMotion="user"and a globalprefers-reduced-motionrule cover everything else. - Images: WebP with explicit
width/height; below-the-fold images getloading="lazy".
- ⌘K / Ctrl+K command palette: jump to sections, copy email, download the resume, open external links. Loaded on first use.
- Accessible by default: skip link, visible focus rings, keyboard-reachable tooltips, labelled form fields with inline errors and focus-on-first-error, 44px touch targets.
- Security: Content-Security-Policy via
<meta>, pinned CDN stylesheet with Subresource Integrity, no inline scripts.
npm run dev # dev server on http://localhost:5173
npm run build # production build to dist/
npm run lint # eslintPush to main. The Deploy to GitHub Pages workflow installs with
npm ci --ignore-scripts, runs npm audit --audit-level=high, builds, and publishes dist/ to
GitHub Pages. There is no manual deploy step.
If a deploy fails at the audit step, it's usually a new advisory in a dev-only dependency:
run npm audit fix, confirm the build still passes, and push the updated lockfile.
- Resume: replace
public/resume.pdf. It downloads asRyan_Wetzstein_Resume.pdf. - Copy style: write "resume" without accents, and avoid em dashes in anything visitors see.