Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pnpm-debug.log*
playwright-report/
test-results/

# worktrees
.worktrees/

# project-local tool dirs
.obsidian/
.vscode/
Expand Down
52 changes: 52 additions & 0 deletions e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,55 @@ test.describe("Accessibility", () => {
expect(results.violations).toEqual([]);
});
});

test.describe("Accessibility — Light mode", () => {
test("homepage in light mode has no axe-core violations", async ({
page,
}) => {
await page.goto("/");
await page.waitForLoadState("networkidle");

// Apply light theme directly — tests that light mode CSS has no a11y violations
await page.evaluate(() =>
document.documentElement.setAttribute("data-theme", "light"),
);

const theme = await page.evaluate(() =>
document.documentElement.getAttribute("data-theme"),
);
expect(theme).toBe("light");

await page.evaluate(async () => {
await new Promise<void>((resolve) => {
const distance = 300;
const delay = 100;
const timer = setInterval(() => {
window.scrollBy(0, distance);
if (
window.scrollY + window.innerHeight >=
document.body.scrollHeight
) {
clearInterval(timer);
window.scrollTo(0, 0);
resolve();
}
}, delay);
});
});
await page.waitForTimeout(500);

const results = await new AxeBuilder({ page }).analyze();

if (results.violations.length > 0) {
const report = results.violations
.map(
(v) =>
`[${v.impact}] ${v.id}: ${v.description}\n Nodes: ${v.nodes.map((n) => n.html).join(", ")}`,
)
.join("\n\n");
console.error("Axe violations (light mode):\n" + report);
}

expect(results.violations).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion src/components/Contact.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { config } from "../data/config";
<section id="contact" aria-label="Contact" class="py-24 px-6 border-t border-[#1e1e1e]">
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-12 font-mono text-xs text-white/50 tracking-widest">
<span>// CONTACT</span><span>06 / 06</span>
<h2 class="font-mono text-xs text-white/50 tracking-widest">// CONTACT</h2><span>06 / 06</span>
</div>
<h2 class="text-6xl font-black tracking-tight leading-none mb-12 uppercase">
LET'S BUILD SOMETHING<br />GREAT TOGETHER.
Expand Down
4 changes: 2 additions & 2 deletions src/components/Experience.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import { experiences } from "../data/experiences";
---

<section id="experience" aria-label="Experience" class="py-24 px-6 border-t border-[#1e1e1e]">
<section id="experience" aria-label="Experience" class="py-24 px-6 border-t border-(--border)">
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-12 font-mono text-xs text-white/50 tracking-widest">
<h2 class="font-mono text-xs text-white/50 tracking-widest">// EXPERIENCE</h2><span>04 / 06</span>
</div>
<ol id="experience-list" class="list-none p-0 m-0 flex flex-col divide-y divide-[#1e1e1e]">
<ol id="experience-list" class="list-none p-0 m-0 flex flex-col divide-y divide-(--border)">
{experiences.map((exp) => (
<li class="list-none">
<article class="experience-item py-8">
Expand Down
4 changes: 2 additions & 2 deletions src/components/Faq.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ const faqs = [
];
---

<section id="faq" aria-label="FAQ" class="py-24 px-6 border-t border-[#1e1e1e]">
<section id="faq" aria-label="FAQ" class="py-24 px-6 border-t border-(--border)">
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-12 font-mono text-xs text-white/50 tracking-widest">
<h2 class="font-mono text-xs text-white/50 tracking-widest">// FAQ</h2><span>05 / 06</span>
</div>
<dl class="flex flex-col divide-y divide-[#1e1e1e]">
<dl class="flex flex-col divide-y divide-(--border)">
{
faqs.map((faq) => (
<div class="py-6">
Expand Down
23 changes: 19 additions & 4 deletions src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const colorData = JSON.stringify(colorRows);
<canvas
id="ascii-art"
class="block"
style="background: #0a0a0a; border-radius: 8px;"
aria-label="ASCII portrait of Gabriel Raymondou"></canvas>
<script is:inline type="application/json" id="ascii-colors" set:html={colorData} />
<div class="flex gap-4 mt-2 hidden md:flex" aria-hidden="true">
Expand Down Expand Up @@ -205,6 +206,10 @@ const colorData = JSON.stringify(colorRows);
return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}

function getBgColor() {
return '#0a0a0a';
}

function drawFrame(revealRow: number, glitchProb = 0.5) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = FONT;
Expand All @@ -216,7 +221,7 @@ const colorData = JSON.stringify(colorRows);
let x = 0;
for (const [r, g, b, charCode] of row) {
if (r + g + b < 20) {
ctx.fillStyle = "#0a0a0a";
ctx.fillStyle = getBgColor();
} else if (isDesktop) {
const l = lum(r, g, b);
ctx.fillStyle = `rgb(${l},${l},${l})`;
Expand Down Expand Up @@ -247,7 +252,7 @@ const colorData = JSON.stringify(colorRows);
const dist = Math.hypot(x + charWidth / 2 - mouseX, y - mouseY);
const blend = r + g + b < 20 ? 0 : Math.max(0, 1 - dist / SPOTLIGHT_RADIUS);
if (blend === 0 && r + g + b < 20) {
ctx.fillStyle = "#0a0a0a";
ctx.fillStyle = getBgColor();
} else {
const l = lum(r, g, b);
ctx.fillStyle = `rgb(${Math.round(l + (r - l) * blend)},${Math.round(l + (g - l) * blend)},${Math.round(l + (b - l) * blend)})`;
Expand All @@ -271,7 +276,7 @@ const colorData = JSON.stringify(colorRows);
const y = rowIdx * LINE_HEIGHT + FONT_SIZE;
let x = 0;
for (const [r, g, b, charCode] of row) {
ctx.fillStyle = r + g + b < 20 ? "#0a0a0a" : `rgb(${r},${g},${b})`;
ctx.fillStyle = r + g + b < 20 ? getBgColor() : `rgb(${r},${g},${b})`;
ctx.fillText(String.fromCharCode(charCode), x, y);
x += charWidth;
}
Expand All @@ -298,10 +303,11 @@ const colorData = JSON.stringify(colorRows);
});
}

let locked = false;

if (isDesktop) {
let mouse = { x: -9999, y: -9999 };
let rafId: number | null = null;
let locked = false;

canvas.addEventListener("click", () => {
tween?.kill();
Expand Down Expand Up @@ -336,4 +342,13 @@ const colorData = JSON.stringify(colorRows);
drawFrame(colorRows.length);
});
}

window.addEventListener('theme-changed', () => {
if (tween?.isActive()) return;
if (locked) {
drawFullColor();
} else {
drawFrame(colorRows.length);
}
});
</script>
63 changes: 47 additions & 16 deletions src/components/Navbar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@
style="background: transparent;"
>
<span class="font-mono text-sm tracking-widest text-white/60">Raigato<span aria-hidden="true"> —</span></span>
<a
href="#contact"
class="font-mono text-xs tracking-widest bg-white text-black px-4 py-2 hover:bg-white/85 transition-all duration-200 uppercase"
>
GET IN TOUCH →
</a>
<div class="flex items-center gap-3">
<button
id="theme-toggle"
aria-label="Toggle theme"
class="font-mono text-xs tracking-widest px-2 py-1 transition-colors duration-200"
style="color: var(--fg);"
>
[dark_mode]
</button>
<a
href="#contact"
class="font-mono text-xs tracking-widest bg-white text-black px-4 py-2 hover:bg-white/85 transition-all duration-200 uppercase"
>
GET IN TOUCH →
</a>
</div>
</nav>

<script>
Expand All @@ -22,22 +32,43 @@
gsap.registerPlugin(ScrollTrigger)

const navbar = document.getElementById('navbar')
const toggle = document.getElementById('theme-toggle')

ScrollTrigger.create({
start: 'top -80px',
onEnter: () => {
gsap.to(navbar, {
backgroundColor: 'rgba(10, 10, 10, 0.95)',
borderBottom: '1px solid #1e1e1e',
duration: 0,
})
navbar!.style.backgroundColor = 'color-mix(in srgb, var(--bg) 95%, transparent)'
navbar!.style.borderBottom = '1px solid var(--border)'
},
onLeaveBack: () => {
gsap.to(navbar, {
backgroundColor: 'transparent',
borderBottom: 'none',
duration: 0,
})
navbar!.style.backgroundColor = 'transparent'
navbar!.style.borderBottom = 'none'
},
})

toggle?.addEventListener('click', () => {
window.addEventListener('theme-transition-complete', () => {
if (toggle) {
const isLight = document.documentElement.getAttribute('data-theme') === 'light'
toggle.textContent = isLight ? '[light_mode]' : '[dark_mode]'
}
}, { once: true })

;(window as Window & { triggerThemeTransition: (cb: () => void) => void }).triggerThemeTransition(() => {
const isLight = document.documentElement.getAttribute('data-theme') === 'light'
if (isLight) {
document.documentElement.removeAttribute('data-theme')
localStorage.setItem('theme', 'dark')
} else {
document.documentElement.setAttribute('data-theme', 'light')
localStorage.setItem('theme', 'light')
}
window.dispatchEvent(new CustomEvent('theme-changed'))
})
})

// Set initial label based on saved preference
if (localStorage.getItem('theme') === 'light' && toggle) {
toggle.textContent = '[light_mode]'
}
</script>
66 changes: 66 additions & 0 deletions src/components/ThemeTransition.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script>
const ASCII = '!@#$%^&*()[]{}|<>/\\;:░▒▓█▄▀■□▪▫0123456789ABCDEFabcdef'

declare global {
interface Window {
triggerThemeTransition: (callback: () => void) => void
}
}

window.triggerThemeTransition = function (callback: () => void) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
callback()
window.dispatchEvent(new CustomEvent('theme-transition-complete'))
return
}

const selectors = 'h1, h2, h3, h4, nav a, button, .font-mono'
const targets = [...document.querySelectorAll<HTMLElement>(selectors)].filter((el) => {
const text = el.textContent?.trim() ?? ''
return text.length > 0 && text.length <= 60 && !el.querySelector(selectors)
})

const originals = targets.map((el) => el.textContent ?? '')
const originalHTMLs = targets.map((el) => el.innerHTML)
let frame = 0
const totalFrames = 20
let callbackCalled = false

const interval = setInterval(() => {
frame++
const progress = frame / totalFrames

targets.forEach((el, i) => {
const orig = originals[i]
const resolveThreshold = totalFrames * 0.7
const scrambled = orig
.split('')
.map((ch, ci) => {
if (ch === ' ' || ch === '\n') return ch
// Resolve characters from the start once past 70% progress
if (
frame > resolveThreshold &&
ci < Math.floor(((progress - 0.7) / 0.3) * orig.length)
)
return ch
return ASCII[Math.floor(Math.random() * ASCII.length)]
})
.join('')
el.textContent = scrambled
})

if (frame === Math.floor(totalFrames * 0.5) && !callbackCalled) {
callbackCalled = true
callback()
}

if (frame >= totalFrames) {
clearInterval(interval)
targets.forEach((el, i) => {
el.innerHTML = originalHTMLs[i]
})
window.dispatchEvent(new CustomEvent('theme-transition-complete'))
}
}, 30)
}
</script>
8 changes: 8 additions & 0 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ import Hero from "../components/Hero.astro";
import Navbar from "../components/Navbar.astro";
import Projects from "../components/Projects.astro";
import PostHog from "../components/posthog.astro";
import ThemeTransition from "../components/ThemeTransition.astro";
---

<html lang="en">
<head>
<PostHog />
<meta charset="utf-8" />
<script is:inline>
(function() {
const saved = localStorage.getItem('theme');
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="google-site-verification" content="hSsAKq4y67A8Oiq6icLQ0ww4fV8MmW93lqWbwbEVv4w" />
<meta name="robots" content="index, follow" />
Expand Down Expand Up @@ -162,5 +169,6 @@ import PostHog from "../components/posthog.astro";
<Contact />
</main>
<Footer />
<ThemeTransition />
</body>
</html>
Loading
Loading