Skip to content

Commit 6b2e26b

Browse files
committed
feat(theme): add charcoal & chalk dark mode
- Introduced a new class-based .dark theme in global.css - Replaced hardcoded ink canvas colors with CSS variables - Replaced mix-blend-mode: multiply with color-dodge/screen for dark theme - Added MutationObserver to InkCanvas.astro to redraw squiggles on theme flip - Removed all border-radius styles universally for a sharp editorial look - Added a FOUC-preventing ThemeToggle.astro component
1 parent 996e355 commit 6b2e26b

4 files changed

Lines changed: 152 additions & 11 deletions

File tree

‎src/components/InkCanvas.astro‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
mix-blend-mode: multiply;
3535
filter: url(#rough-ink);
3636
opacity: 0.85;
37+
transition: mix-blend-mode 0.3s var(--ease), opacity 0.3s var(--ease);
38+
}
39+
40+
html.dark #ink-canvas {
41+
mix-blend-mode: screen;
42+
opacity: 0.8;
3743
}
3844
</style>
3945

@@ -44,6 +50,15 @@
4450
let width: number, height: number;
4551
let dpr = Math.min(window.devicePixelRatio || 1, 2);
4652

53+
function hexToRgba(hex: string, alpha: number) {
54+
let c = hex.trim().replace('#', '');
55+
if (c.length === 3) c = c.split('').map(x => x + x).join('');
56+
const r = parseInt(c.slice(0, 2), 16) || 0;
57+
const g = parseInt(c.slice(2, 4), 16) || 0;
58+
const b = parseInt(c.slice(4, 6), 16) || 0;
59+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
60+
}
61+
4762
function resize() {
4863
const rect = canvas.parentElement!.getBoundingClientRect();
4964
width = rect.width;
@@ -80,10 +95,16 @@
8095
});
8196
}
8297

98+
const isDark = document.documentElement.classList.contains('dark');
99+
const rootStyles = getComputedStyle(document.documentElement);
100+
const inkHex = rootStyles.getPropertyValue('--ink').trim() || (isDark ? '#e8e4dc' : '#1c1917');
101+
const inkFaintHex = rootStyles.getPropertyValue('--ink-faint').trim() || (isDark ? '#7a746d' : '#a8a29e');
102+
const accentHex = rootStyles.getPropertyValue('--accent').trim() || (isDark ? '#e06c53' : '#9e3b2d');
103+
83104
const colors = [
84-
'rgba(28, 25, 23, 0.18)', 'rgba(28, 25, 23, 0.14)', 'rgba(28, 25, 23, 0.12)',
85-
'rgba(87, 83, 78, 0.13)', 'rgba(158, 59, 45, 0.10)', 'rgba(158, 59, 45, 0.08)',
86-
'rgba(120, 113, 108, 0.11)', 'rgba(28, 25, 23, 0.20)'
105+
hexToRgba(inkHex, 0.18), hexToRgba(inkHex, 0.14), hexToRgba(inkHex, 0.12),
106+
hexToRgba(inkFaintHex, 0.13), hexToRgba(accentHex, 0.10), hexToRgba(accentHex, 0.08),
107+
hexToRgba(inkHex, 0.11), hexToRgba(inkHex, 0.20)
87108
];
88109
this.color = colors[Math.floor(Math.random() * colors.length)];
89110
this.baseWidth = 1 + Math.random() * 4;
@@ -172,10 +193,14 @@
172193
let dots: InkDot[] = [];
173194

174195
function drawDots() {
196+
const isDark = document.documentElement.classList.contains('dark');
197+
const rootStyles = getComputedStyle(document.documentElement);
198+
const inkHex = rootStyles.getPropertyValue('--ink').trim() || (isDark ? '#e8e4dc' : '#1c1917');
199+
175200
for (const d of dots) {
176201
ctx.beginPath();
177202
ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
178-
ctx.fillStyle = `rgba(28, 25, 23, ${d.opacity})`;
203+
ctx.fillStyle = hexToRgba(inkHex, d.opacity);
179204
ctx.fill();
180205
}
181206
}
@@ -221,4 +246,14 @@
221246
init();
222247

223248
window.addEventListener('resize', () => { init(); });
249+
250+
const observer = new MutationObserver((mutations) => {
251+
for (const m of mutations) {
252+
if (m.attributeName === 'class') {
253+
init();
254+
break;
255+
}
256+
}
257+
});
258+
observer.observe(document.documentElement, { attributes: true });
224259
</script>

‎src/components/ThemeToggle.astro‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
---
3+
4+
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
5+
<span class="theme-toggle__bracket">[</span>
6+
<span class="theme-toggle__text">dark</span>
7+
<span class="theme-toggle__bracket">]</span>
8+
</button>
9+
10+
<style>
11+
.theme-toggle {
12+
background: none;
13+
border: none;
14+
cursor: pointer;
15+
font-family: var(--font-mono);
16+
font-size: var(--text-sm);
17+
color: var(--ink-subtle);
18+
padding: 0;
19+
margin-left: var(--space-md);
20+
transition: color 0.2s var(--ease);
21+
display: inline-flex;
22+
align-items: baseline;
23+
gap: 0.2em;
24+
}
25+
26+
.theme-toggle:hover {
27+
color: var(--ink);
28+
}
29+
30+
.theme-toggle__bracket {
31+
color: var(--border-dark);
32+
}
33+
34+
.theme-toggle:hover .theme-toggle__bracket {
35+
color: var(--ink-faint);
36+
}
37+
</style>
38+
39+
<script>
40+
const toggle = document.getElementById('theme-toggle');
41+
const text = toggle?.querySelector('.theme-toggle__text');
42+
43+
function updateText() {
44+
if (text) {
45+
text.textContent = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
46+
}
47+
}
48+
49+
// Set initial text on load
50+
updateText();
51+
52+
toggle?.addEventListener('click', () => {
53+
document.documentElement.classList.toggle('dark');
54+
const isDark = document.documentElement.classList.contains('dark');
55+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
56+
updateText();
57+
});
58+
59+
// Listen for OS theme changes if user hasn't explicitly set a preference
60+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
61+
if (!localStorage.getItem('theme')) {
62+
if (e.matches) {
63+
document.documentElement.classList.add('dark');
64+
} else {
65+
document.documentElement.classList.remove('dark');
66+
}
67+
updateText();
68+
}
69+
});
70+
</script>

‎src/layouts/Base.astro‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
import Icon from '../components/Icon.astro';
2+
import ThemeToggle from '../components/ThemeToggle.astro';
33
44
interface Props {
55
title: string;
@@ -18,19 +18,34 @@ const currentPath = Astro.url.pathname;
1818
<meta name="description" content={description} />
1919
<title>{title}</title>
2020
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
21+
<script is:inline>
22+
const theme = (() => {
23+
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
24+
return localStorage.getItem('theme');
25+
}
26+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
27+
return 'dark';
28+
}
29+
return 'light';
30+
})();
31+
if (theme === 'dark') {
32+
document.documentElement.classList.add('dark');
33+
}
34+
</script>
2135
</head>
2236
<body>
2337
<header class="site-header">
2438
<div class="container container--wide">
2539
<div class="site-header__inner">
2640
<a href="/" class="site-header__name">Abhyudit Singh</a>
27-
<nav>
41+
<nav style="display: flex; align-items: baseline;">
2842
<ul class="site-nav">
2943
<li><a href="/projects" class:list={[{ active: currentPath.startsWith('/projects') }]}>projects</a></li>
3044
<li><a href="/research" class:list={[{ active: currentPath.startsWith('/research') }]}>research</a></li>
3145
<li><a href="/blog" class:list={[{ active: currentPath.startsWith('/blog') }]}>blog</a></li>
3246
<li><a href="/misc" class:list={[{ active: currentPath.startsWith('/misc') }]}>misc</a></li>
3347
</ul>
48+
<ThemeToggle />
3449
</nav>
3550
</div>
3651
</div>

‎src/styles/global.css‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@
6767
--ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
6868
}
6969

70+
html.dark {
71+
/* Paper tones -> Charcoal tones */
72+
--bg: #161615;
73+
--bg-warm: #1c1a19;
74+
--bg-card: #211f1e;
75+
--border: #33302e;
76+
--border-dark: #4a4542;
77+
78+
/* Ink tones -> Chalk tones */
79+
--ink: #e8e4dc;
80+
--ink-light: #b8b2a7;
81+
--ink-faint: #7a746d;
82+
--ink-subtle: #968f86;
83+
84+
/* Accent */
85+
--accent: #e06c53;
86+
--accent-hover: #fca5a5;
87+
--accent-glow: rgba(224, 108, 83, 0.15);
88+
--accent-soft: #b35a47;
89+
}
90+
7091
/* ── Base ──────────────────────────────── */
7192

7293
html {
@@ -172,7 +193,7 @@ code {
172193
font-size: 0.88em;
173194
background: var(--bg-warm);
174195
padding: 0.15em 0.4em;
175-
border-radius: 3px;
196+
border-radius: 0;
176197
color: var(--ink);
177198
border: 1px solid var(--border);
178199
}
@@ -381,7 +402,7 @@ hr {
381402
padding: var(--space-md) var(--space-lg);
382403
background: var(--accent-glow);
383404
border: 1px solid rgba(158, 59, 45, 0.12);
384-
border-radius: 6px;
405+
border-radius: 0;
385406
margin: var(--space-xl) 0;
386407
transition: border-color 0.3s var(--ease);
387408
}
@@ -438,7 +459,7 @@ hr {
438459
width: 48px;
439460
height: 2px;
440461
background: var(--accent);
441-
border-radius: 1px;
462+
border-radius: 0;
442463
}
443464

444465
/* ═══════════════════════════════════════════
@@ -510,7 +531,7 @@ hr {
510531
color: var(--ink-subtle);
511532
background: var(--bg-warm);
512533
padding: 0.2em 0.6em;
513-
border-radius: 3px;
534+
border-radius: 0;
514535
white-space: nowrap;
515536
border: 1px solid var(--border);
516537
transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
@@ -735,7 +756,7 @@ hr {
735756
width: 32px;
736757
height: 2px;
737758
background: var(--accent);
738-
border-radius: 1px;
759+
border-radius: 0;
739760
}
740761

741762
.misc-item {

0 commit comments

Comments
 (0)