diff --git a/package.json b/package.json index d6923ff..da8ebc0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "themockingjet.github.io", "private": true, - "version": "1.0.0", + "version": "1.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index 2d1c478..37156ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,8 @@ const Skills = lazy(() => import('./components/Skills/Skills')) const portfolio = parsePortfolio(rawPortfolio) export const THEMES = [ - { id: 'warm', label: 'Warm', accent: '#5e8a70' }, + { id: 'amber', label: 'Amber', accent: '#f59e0b' }, + { id: 'sage', label: 'Sage', accent: '#5e8a70' }, { id: 'ocean', label: 'Ocean', accent: '#4a9eff' }, { id: 'ember', label: 'Ember', accent: '#e07840' }, { id: 'violet', label: 'Violet', accent: '#8b67e8' }, @@ -31,16 +32,16 @@ const REVEAL = { export default function App() { const [dark, setDark] = useState(true) - const [theme, setTheme] = useState('warm') + const [theme, setTheme] = useState('amber') const toggleDark = () => setDark((d) => !d) useEffect(() => { - if (dark) document.documentElement.removeAttribute('data-dark') - else document.documentElement.setAttribute('data-dark', '0') + if (dark) document.documentElement.removeAttribute('data-mode') + else document.documentElement.setAttribute('data-mode', 'light') }, [dark]) useEffect(() => { - if (theme === 'warm') document.documentElement.removeAttribute('data-theme') + if (theme === 'amber') document.documentElement.removeAttribute('data-theme') else document.documentElement.setAttribute('data-theme', theme) }, [theme]) diff --git a/src/components/About/About.module.css b/src/components/About/About.module.css index 628bd28..83db1d1 100644 --- a/src/components/About/About.module.css +++ b/src/components/About/About.module.css @@ -1,46 +1,21 @@ /* Section container */ .about { + composes: section-grid from global; position: relative; padding: var(--space-16) 0; + overflow: visible; } -/* Ambient glow — centered on watermark text */ .about::after { content: ''; position: absolute; - left: 5%; - top: 30%; - width: 60vw; - height: 60vw; - transform: translate(-20%, -50%); - background: radial-gradient(circle, var(--color-accent-glow) 0%, transparent 60%); + left: -10vw; + bottom: -8vw; + width: 45vw; + height: 45vw; + background: radial-gradient(ellipse at center, color-mix(in srgb, var(--color-accent) 10%, transparent) 0%, transparent 65%); pointer-events: none; z-index: 0; - opacity: 0.35; -} - -/* Background watermark */ -.decor { - position: absolute; - left: 5%; - top: 30%; - font-size: 22vw; - font-weight: 800; - line-height: 1; - white-space: nowrap; - background: radial-gradient(ellipse at center, - color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, - color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, - color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, - transparent 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - pointer-events: none; - user-select: none; - letter-spacing: -0.04em; - opacity: 0.5; } .inner { @@ -71,17 +46,6 @@ .prose { max-width: 58ch; position: relative; - padding-left: var(--space-6); -} - -.prose::before { - content: ''; - position: absolute; - left: 0; - top: 4px; - bottom: 4px; - width: 2px; - background: linear-gradient(to bottom, var(--color-accent), transparent); } /* Paragraph block (one per intent) */ @@ -126,16 +90,6 @@ border-bottom: 1px solid transparent; } -@media (max-width: 1024px) { - .decor { - display: none; - } - - .about::after { - display: none; - } -} - @media (max-width: 767px) { .about { padding: var(--space-12) 0; diff --git a/src/components/About/About.tsx b/src/components/About/About.tsx index 5cf16ba..464ca96 100644 --- a/src/components/About/About.tsx +++ b/src/components/About/About.tsx @@ -1,8 +1,8 @@ -import { motion, AnimatePresence, useScroll, useTransform } from 'motion/react' +import { motion, AnimatePresence } from 'motion/react' import { useEffect, useState, useRef } from 'react' import styles from './About.module.css' import type { AboutData } from '../../lib/parsePortfolio' -import { EASE, STAGGER } from '../../lib/motion' +import { EASE, STAGGER, FADE_UP } from '../../lib/motion' interface AboutProps { data: AboutData @@ -13,11 +13,6 @@ const FADE_MS = 0.35 const ENTER = STAGGER -const SLOT_ENTER = { - hidden: { opacity: 0, y: 12 }, - show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } }, -} - function WordSlot({ variants, personaIdx }: { variants: string[]; personaIdx: number }) { const [hasSwapped, setHasSwapped] = useState(false) const prevIdx = useRef(personaIdx) @@ -60,12 +55,6 @@ export default function About({ data }: AboutProps) { const [ready, setReady] = useState(false) const sectionRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [70, -110]) - useEffect(() => { const t = setTimeout(() => setReady(true), 1800) return () => clearTimeout(t) @@ -82,9 +71,6 @@ export default function About({ data }: AboutProps) { return (
-
{slots.map(({ label, segments }, i) => ( - + // {label}

{segments.map((seg, j) => diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index 90a9487..fdf7873 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -2,17 +2,12 @@ import { motion, useScroll, useTransform } from 'motion/react' import { useEffect, useRef, useState } from 'react' import styles from './Hero.module.css' import type { HeroData } from '../../lib/parsePortfolio' -import { EASE } from '../../lib/motion' +import { EASE, STAGGER_DELAY } from '../../lib/motion' interface HeroProps { data: HeroData } -const container = { - hidden: {}, - show: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } }, -} - const item = { hidden: { opacity: 0, y: 18 }, show: { opacity: 1, y: 0, transition: { duration: 0.65, ease: EASE } }, @@ -90,7 +85,7 @@ export default function Hero({ data }: HeroProps) { diff --git a/src/components/Projects/Projects.module.css b/src/components/Projects/Projects.module.css index d352509..8745c51 100644 --- a/src/components/Projects/Projects.module.css +++ b/src/components/Projects/Projects.module.css @@ -1,46 +1,21 @@ /* Section container */ .projects { + composes: section-grid from global; position: relative; padding: var(--space-16) 0; + overflow: visible; } -/* Ambient glow — centered on watermark text */ .projects::after { content: ''; position: absolute; - left: 5%; - top: 50%; - width: 60vw; - height: 60vw; - transform: translate(-20%, -50%); - background: radial-gradient(circle, var(--color-accent-glow) 0%, transparent 60%); + right: -5vw; + top: -6vw; + width: 40vw; + height: 40vw; + background: radial-gradient(ellipse at center, color-mix(in srgb, var(--color-accent) 8%, transparent) 0%, transparent 65%); pointer-events: none; z-index: 0; - opacity: 0.4; -} - -/* Background watermark */ -.decor { - position: absolute; - left: 5%; - top: 50%; - font-size: 22vw; - font-weight: 800; - line-height: 1; - white-space: nowrap; - background: radial-gradient(ellipse at center, - color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, - color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, - color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, - transparent 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - pointer-events: none; - user-select: none; - letter-spacing: -0.04em; - opacity: 0.5; } .inner { @@ -66,9 +41,11 @@ .card { --card-accent: var(--color-accent); position: relative; - background: var(--color-bg-surface); - border: 1px dashed var(--color-border); - border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; padding: var(--space-5) var(--space-5) var(--space-5) var(--space-6); will-change: transform, opacity; display: grid; @@ -77,10 +54,12 @@ column-gap: var(--space-4); row-gap: var(--space-2); overflow: hidden; + box-shadow: 0 2px 16px -4px rgba(0, 0, 0, 0.3); transition: + box-shadow 0.3s ease, border-color 0.3s ease, background 0.3s ease, - box-shadow 0.3s ease; + transform 0.3s ease; } /* Corner bracket — top-left */ @@ -115,11 +94,12 @@ } .card:hover { - border-color: color-mix(in srgb, var(--color-border) 40%, var(--color-accent)); - background: var(--color-bg-elevated); + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); box-shadow: - 0 0 0 1px var(--color-accent-dim), - 0 4px 24px -4px rgba(0, 0, 0, 0.4); + 0 4px 24px -4px rgba(0, 0, 0, 0.4), + 0 0 0 1px var(--color-accent-dim); } .card:hover::before, @@ -330,21 +310,23 @@ gap: 4px; font-family: var(--font-mono); font-size: var(--text-xs); - color: var(--color-text-faint); - background: none; - border: 1px dashed var(--color-border); + color: var(--color-text-muted); + background: var(--color-bg-hover); + border: 1px solid var(--color-border); border-radius: 2px; - padding: 3px 10px; + padding: 4px 12px; letter-spacing: 0.04em; text-transform: uppercase; width: fit-content; margin-top: var(--space-2); - transition: color 0.2s ease, border-color 0.2s ease; + cursor: pointer; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; } .expandBtn:hover { color: var(--color-accent); border-color: var(--color-accent); + background: var(--color-accent-dim); } /* Details panel */ @@ -417,20 +399,13 @@ opacity: 1; } -.archivedList .card::before { +.archivedList .card::before, +.archivedList .card::after, +.archivedList .cornerBL, +.archivedList .cornerBR { border-color: var(--color-text-faint); } -@media (max-width: 1024px) { - .decor { - display: none; - } - - .projects::after { - display: none; - } -} - @media (max-width: 767px) { .projects { padding: var(--space-12) 0; diff --git a/src/components/Projects/Projects.tsx b/src/components/Projects/Projects.tsx index 63019e1..98bb56b 100644 --- a/src/components/Projects/Projects.tsx +++ b/src/components/Projects/Projects.tsx @@ -1,10 +1,10 @@ import { useState, useRef } from 'react' -import { motion, AnimatePresence, useScroll, useTransform } from 'motion/react' +import { motion, AnimatePresence } from 'motion/react' import { Icon } from '@iconify/react' import styles from './Projects.module.css' import type { ProjectData } from '../../lib/parsePortfolio' import { resolveIcon } from '../../lib/icons' -import { EASE, STAGGER } from '../../lib/motion' +import { EASE, STAGGER, FADE_UP } from '../../lib/motion' const STATUS_CONFIG: Record = { active: { label: 'Active', color: 'var(--status-active)' }, @@ -16,11 +16,6 @@ function resolveStatus(status: string) { return STATUS_CONFIG[status] ?? { label: status, color: 'var(--text-muted)' } } -const CARD = { - hidden: { opacity: 0, y: 18 }, - show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } }, -} - interface ProjectsProps { @@ -67,7 +62,7 @@ function ProjectCard({ project, index, featured }: { project: ProjectData; index ].filter(Boolean).join(' ') return ( - + {indexLabel} @@ -134,17 +129,8 @@ export default function Projects({ projects }: ProjectsProps) { const archived = projects.filter((p) => p.status === 'deprecated') const sectionRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [80, -120]) - return (

-
+

{label}

{items.map((item) => { @@ -47,17 +42,8 @@ function SkillGroup({ label, items }: { label: string; items: string[] }) { export default function Skills({ skills }: SkillsProps) { const sectionRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [60, -100]) - return (
-
{ await page.goto('/') // Toggle to light await page.getByRole('button', { name: /switch to light/i }).click() - const dark = await page.locator('html').getAttribute('data-dark') - expect(dark).toBe('0') + const dark = await page.locator('html').getAttribute('data-mode') + expect(dark).toBe('light') // Toggle back to dark await page.getByRole('button', { name: /switch to dark/i }).click() - const darkAfter = await page.locator('html').getAttribute('data-dark') + const darkAfter = await page.locator('html').getAttribute('data-mode') expect(darkAfter).toBeNull() }) diff --git a/tests/integration/theme.test.tsx b/tests/integration/theme.test.tsx index d5f53f9..c41f44d 100644 --- a/tests/integration/theme.test.tsx +++ b/tests/integration/theme.test.tsx @@ -5,11 +5,11 @@ import App from '../../src/App' describe('Theme switching', () => { beforeEach(() => { document.documentElement.removeAttribute('data-theme') - document.documentElement.removeAttribute('data-dark') + document.documentElement.removeAttribute('data-mode') }) describe('given the default state', () => { - it('should start with the warm theme (no data-theme attribute)', () => { + it('should start with the amber theme (no data-theme attribute)', () => { render() expect(document.documentElement.getAttribute('data-theme')).toBeNull() }) @@ -24,14 +24,14 @@ describe('Theme switching', () => { expect(document.documentElement.getAttribute('data-theme')).toBe('ocean') }) - it('should switch back to warm when selected again', async () => { + it('should switch back to amber when selected again', async () => { const user = userEvent.setup() render() const themeBtn = screen.getByRole('button', { name: /theme/i }) await user.click(themeBtn) await user.click(screen.getByRole('option', { name: /ocean/i })) await user.click(themeBtn) - await user.click(screen.getByRole('option', { name: /warm/i })) + await user.click(screen.getByRole('option', { name: /amber/i })) expect(document.documentElement.getAttribute('data-theme')).toBeNull() }) @@ -48,22 +48,22 @@ describe('Theme switching', () => { describe('Dark mode toggle', () => { beforeEach(() => { - document.documentElement.removeAttribute('data-dark') + document.documentElement.removeAttribute('data-mode') }) describe('given the default dark mode', () => { - it('should start without a data-dark attribute', () => { + it('should start without a data-mode attribute', () => { render() - expect(document.documentElement.getAttribute('data-dark')).toBeNull() + expect(document.documentElement.getAttribute('data-mode')).toBeNull() }) }) describe('when the user toggles to light mode', () => { - it('should set data-dark="0" on the document', async () => { + it('should set data-mode="light" on the document', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: /switch to light/i })) - expect(document.documentElement.getAttribute('data-dark')).toBe('0') + expect(document.documentElement.getAttribute('data-mode')).toBe('light') }) it('should toggle back to dark mode on second click', async () => { @@ -71,7 +71,7 @@ describe('Dark mode toggle', () => { render() await user.click(screen.getByRole('button', { name: /switch to light/i })) await user.click(screen.getByRole('button', { name: /switch to dark/i })) - expect(document.documentElement.getAttribute('data-dark')).toBeNull() + expect(document.documentElement.getAttribute('data-mode')).toBeNull() }) }) })