Skip to content

Commit d55b3a2

Browse files
committed
feat: dual themes
1 parent 0d87de4 commit d55b3a2

7 files changed

Lines changed: 857 additions & 509 deletions

File tree

src/layouts/BaseLayout.astro

Lines changed: 318 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ export interface Props {
77
const { title, description = "Johan Wulf - Software Engineer" } = Astro.props
88
const pathname = Astro.url.pathname
99
10+
// Terminal theme navigation
11+
const navItems = [
12+
{ path: '/', label: '~/' },
13+
{ path: '/blog', label: '~/blog' },
14+
{ path: '/projects', label: '~/projects' },
15+
{ path: '/about', label: '~/about' },
16+
]
17+
1018
const dockItems = [
1119
{
1220
id: 'about',
@@ -32,18 +40,25 @@ const dockItems = [
3240
label: 'Contact',
3341
color: 'from-orange-500 to-orange-600'
3442
},
43+
{
44+
id: 'terminal',
45+
icon: 'terminal',
46+
label: 'Terminal',
47+
color: 'from-gray-700 to-gray-900'
48+
},
3549
]
3650
3751
const iconPaths = {
3852
person: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
3953
folder: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z",
4054
document: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
41-
mail: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
55+
mail: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
56+
terminal: "M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
4257
}
4358
---
4459

4560
<!DOCTYPE html>
46-
<html lang="en">
61+
<html lang="en" class="loading">
4762
<head>
4863
<meta charset="UTF-8">
4964
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -56,6 +71,11 @@ const iconPaths = {
5671
<meta name="apple-mobile-web-app-status-bar-style" content="default">
5772
<meta name="apple-mobile-web-app-title" content="Johan Wulf">
5873

74+
<!-- Fonts for terminal theme -->
75+
<link rel="preconnect" href="https://fonts.googleapis.com">
76+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
77+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
78+
5979
<!-- OpenGraph / Social Media -->
6080
<meta property="og:type" content="website">
6181
<meta property="og:title" content={title}>
@@ -69,9 +89,95 @@ const iconPaths = {
6989
<meta name="twitter:description" content={description}>
7090
<meta name="twitter:image" content={`${Astro.site || 'https://wulf.gg'}/og-image.png`}>
7191
</head>
72-
<body>
73-
<!-- Desktop Environment -->
74-
<div class="desktop">
92+
<body class="opacity-0">
93+
<!-- Loading overlay -->
94+
<div id="loading-overlay" class="fixed inset-0 bg-base z-[100] flex flex-col items-center justify-center">
95+
<div class="mb-8">
96+
<pre class="text-blue text-xs leading-none font-mono whitespace-pre"> _ _ _ __
97+
(_) ___ | |__ __ _ _ ____ ___ _| |/ _|
98+
| |/ _ \| '_ \ / _` | '_ \ \ /\ / / | | | | |_
99+
| | (_) | | | | (_| | | | \ V V /| |_| | | _|
100+
_/ |\___/|_| |_|\__,_|_| |_|\_/\_/ \__,_|_|_|
101+
|__/ </pre>
102+
</div>
103+
<div class="w-64 mb-4">
104+
<div class="bg-surface-0 rounded-full h-2 overflow-hidden">
105+
<div id="loading-progress" class="bg-blue h-full transition-all duration-100 ease-out" style="width: 0%"></div>
106+
</div>
107+
</div>
108+
<div id="loading-text" class="text-overlay-0 text-xs font-mono">Initializing...</div>
109+
</div>
110+
<!-- Theme Toggle Button (hidden in terminal mode) -->
111+
<button id="theme-toggle" class="theme-toggle hidden" title="Back to macOS">
112+
<svg class="w-5 h-5 text-text" fill="none" stroke="currentColor" viewBox="0 0 24 24">
113+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
114+
</svg>
115+
</button>
116+
117+
<!-- Terminal Theme Content -->
118+
<div id="terminal-content" class="hidden terminal-layout">
119+
<!-- Terminal Window Controls -->
120+
<div class="terminal-titlebar">
121+
<div class="terminal-window-controls">
122+
<button id="terminal-close" class="terminal-control close" title="Close Terminal">
123+
<span class="control-icon">×</span>
124+
</button>
125+
<button class="terminal-control minimize" title="Minimize">
126+
<span class="control-icon">−</span>
127+
</button>
128+
<button class="terminal-control maximize" title="Maximize">
129+
<span class="control-icon">+</span>
130+
</button>
131+
</div>
132+
<div class="terminal-title">Terminal — johan@wulf</div>
133+
<div class="w-20"></div>
134+
</div>
135+
136+
<div class="max-w-3xl mx-auto px-8 py-6">
137+
<header class="mb-4">
138+
<pre class="text-blue text-xs sm:text-xs text-[0.6rem] leading-none mb-4 font-mono whitespace-pre ascii-art"> _ _ _ __
139+
(_) ___ | |__ __ _ _ ____ ___ _| |/ _|
140+
| |/ _ \| '_ \ / _` | '_ \ \ /\ / / | | | | |_
141+
| | (_) | | | | (_| | | | \ V V /| |_| | | _|
142+
_/ |\___/|_| |_|\__,_|_| |_|\_/\_/ \__,_|_|_|
143+
|__/ </pre>
144+
</header>
145+
</div>
146+
147+
<div class="sticky top-0 z-10">
148+
<div class="max-w-3xl mx-auto px-8">
149+
<nav class="bg-base py-3 border-b border-surface-0">
150+
<div class="flex gap-8 text-sm">
151+
{navItems.map((item) => {
152+
const isActive = pathname === item.path || (item.path !== '/' && pathname.startsWith(item.path))
153+
return (
154+
<a
155+
href={item.path}
156+
class={isActive ? 'text-text' : 'text-overlay-0 hover:text-text transition-colors'}
157+
>
158+
{item.label}
159+
</a>
160+
)
161+
})}
162+
</div>
163+
</nav>
164+
</div>
165+
</div>
166+
167+
<div class="max-w-3xl mx-auto px-8 pt-8">
168+
<main>
169+
<slot />
170+
</main>
171+
</div>
172+
173+
<footer class="fixed bottom-0 left-1/2 transform -translate-x-1/2 bg-base text-center text-overlay-0 text-xs py-4 max-w-3xl w-full px-8">
174+
<div class="border-t border-surface-0 pt-4">
175+
<p>Interested in collaborating? Reach out!</p>
176+
</div>
177+
</footer>
178+
</div>
179+
<!-- macOS Theme Content -->
180+
<div id="macos-content" class="desktop">
75181
<!-- Menu Bar -->
76182
<div class="menu-bar">
77183
<div class="flex items-center justify-between h-full px-4">
@@ -382,6 +488,201 @@ const iconPaths = {
382488
</style>
383489

384490
<script>
491+
// Theme Management
492+
class ThemeManager {
493+
constructor() {
494+
this.currentTheme = localStorage.getItem('theme') || 'macos';
495+
this.isTransitioning = false;
496+
this.init();
497+
}
498+
499+
init() {
500+
this.applyTheme(this.currentTheme, false);
501+
502+
const toggleButton = document.getElementById('theme-toggle');
503+
toggleButton.addEventListener('click', () => this.toggleToMacOS());
504+
505+
const terminalCloseBtn = document.getElementById('terminal-close');
506+
terminalCloseBtn.addEventListener('click', () => this.toggleToMacOS());
507+
508+
// Prevent page navigation in terminal theme
509+
this.handleTerminalNavigation();
510+
511+
// Hide loading overlay and show content
512+
this.hideLoadingOverlay();
513+
}
514+
515+
hideLoadingOverlay() {
516+
const loadingOverlay = document.getElementById('loading-overlay');
517+
const loadingProgress = document.getElementById('loading-progress');
518+
const loadingText = document.getElementById('loading-text');
519+
const body = document.body;
520+
const html = document.documentElement;
521+
522+
// Check if this is a navigation within terminal theme
523+
const isTerminalNavigation = localStorage.getItem('theme') === 'terminal' &&
524+
document.referrer &&
525+
new URL(document.referrer).origin === window.location.origin;
526+
527+
if (isTerminalNavigation) {
528+
// Skip loading animation for terminal navigation
529+
loadingOverlay.style.display = 'none';
530+
body.style.opacity = '1';
531+
html.classList.remove('loading');
532+
return;
533+
}
534+
535+
const loadingSteps = [
536+
{ progress: 20, text: 'Loading theme preferences...', delay: 300 },
537+
{ progress: 40, text: 'Initializing interface...', delay: 300 },
538+
{ progress: 60, text: 'Setting up navigation...', delay: 300 },
539+
{ progress: 80, text: 'Applying theme...', delay: 300 },
540+
{ progress: 100, text: 'Ready!', delay: 200 }
541+
];
542+
543+
let currentStep = 0;
544+
545+
const runStep = () => {
546+
if (currentStep < loadingSteps.length) {
547+
const step = loadingSteps[currentStep];
548+
loadingProgress.style.width = step.progress + '%';
549+
loadingText.textContent = step.text;
550+
551+
setTimeout(() => {
552+
currentStep++;
553+
runStep();
554+
}, step.delay);
555+
} else {
556+
// Finished loading
557+
setTimeout(() => {
558+
loadingOverlay.style.opacity = '0';
559+
body.style.opacity = '1';
560+
html.classList.remove('loading');
561+
562+
setTimeout(() => {
563+
loadingOverlay.style.display = 'none';
564+
}, 500);
565+
}, 300);
566+
}
567+
};
568+
569+
// Start the loading sequence
570+
setTimeout(() => {
571+
runStep();
572+
}, 200);
573+
}
574+
575+
activateTerminal(dockItem) {
576+
if (this.isTransitioning || this.currentTheme === 'terminal') return;
577+
578+
this.isTransitioning = true;
579+
const terminalContent = document.getElementById('terminal-content');
580+
const macosContent = document.getElementById('macos-content');
581+
const toggleButton = document.getElementById('theme-toggle');
582+
583+
// Get dock item position for zoom origin
584+
const rect = dockItem.getBoundingClientRect();
585+
const centerX = rect.left + rect.width / 2;
586+
const centerY = rect.top + rect.height / 2;
587+
588+
// Set transform origin to dock item position
589+
terminalContent.style.transformOrigin = `${centerX}px ${centerY}px`;
590+
591+
// Start transition
592+
macosContent.classList.add('fade-out-macos');
593+
594+
setTimeout(() => {
595+
macosContent.classList.add('hidden');
596+
terminalContent.classList.remove('hidden');
597+
terminalContent.classList.add('zoom-to-terminal');
598+
599+
this.applyTheme('terminal', false);
600+
toggleButton.classList.remove('hidden');
601+
602+
setTimeout(() => {
603+
terminalContent.classList.remove('zoom-to-terminal');
604+
this.isTransitioning = false;
605+
}, 800);
606+
}, 200);
607+
608+
this.currentTheme = 'terminal';
609+
localStorage.setItem('theme', this.currentTheme);
610+
}
611+
612+
toggleToMacOS() {
613+
if (this.isTransitioning || this.currentTheme === 'macos') return;
614+
615+
this.isTransitioning = true;
616+
const terminalContent = document.getElementById('terminal-content');
617+
const macosContent = document.getElementById('macos-content');
618+
const toggleButton = document.getElementById('theme-toggle');
619+
620+
// Start reverse transition
621+
terminalContent.classList.add('zoom-from-terminal');
622+
623+
setTimeout(() => {
624+
terminalContent.classList.add('hidden');
625+
terminalContent.classList.remove('zoom-from-terminal');
626+
macosContent.classList.remove('hidden');
627+
macosContent.classList.add('fade-in-macos');
628+
629+
this.applyTheme('macos', false);
630+
toggleButton.classList.add('hidden');
631+
632+
setTimeout(() => {
633+
macosContent.classList.remove('fade-in-macos', 'fade-out-macos');
634+
this.isTransitioning = false;
635+
}, 400);
636+
}, 300);
637+
638+
this.currentTheme = 'macos';
639+
localStorage.setItem('theme', this.currentTheme);
640+
}
641+
642+
handleTerminalNavigation() {
643+
// Intercept navigation links in terminal theme
644+
document.addEventListener('click', (e) => {
645+
if (this.currentTheme !== 'terminal') return;
646+
647+
const link = e.target.closest('a');
648+
if (link && link.href && !link.target) {
649+
// Check if it's an internal link
650+
const url = new URL(link.href);
651+
if (url.origin === window.location.origin) {
652+
// Allow navigation but maintain terminal theme state
653+
localStorage.setItem('theme', 'terminal');
654+
}
655+
}
656+
});
657+
}
658+
659+
applyTheme(theme, withAnimation = true) {
660+
const body = document.body;
661+
const html = document.documentElement;
662+
const terminalContent = document.getElementById('terminal-content');
663+
const macosContent = document.getElementById('macos-content');
664+
const toggleButton = document.getElementById('theme-toggle');
665+
666+
if (theme === 'terminal') {
667+
body.className = 'terminal-theme bg-base text-text font-mono min-h-screen pb-16';
668+
html.className = 'terminal-theme';
669+
if (!withAnimation) {
670+
terminalContent.classList.remove('hidden');
671+
macosContent.classList.add('hidden');
672+
toggleButton.classList.remove('hidden');
673+
}
674+
} else {
675+
body.className = '';
676+
html.className = '';
677+
if (!withAnimation) {
678+
terminalContent.classList.add('hidden');
679+
macosContent.classList.remove('hidden');
680+
toggleButton.classList.add('hidden');
681+
}
682+
}
683+
}
684+
}
685+
385686
class WindowManager {
386687
constructor() {
387688
this.windows = new Map();
@@ -396,7 +697,7 @@ const iconPaths = {
396697
item.addEventListener('click', (e) => {
397698
e.preventDefault();
398699
const windowId = item.getAttribute('data-window');
399-
this.toggleWindow(windowId);
700+
this.toggleWindow(windowId, item);
400701
});
401702
});
402703

@@ -408,7 +709,15 @@ const iconPaths = {
408709
});
409710
}
410711

411-
toggleWindow(windowId) {
712+
toggleWindow(windowId, dockItem) {
713+
if (windowId === 'terminal') {
714+
// Handle terminal activation
715+
if (window.themeManager) {
716+
window.themeManager.activateTerminal(dockItem);
717+
}
718+
return;
719+
}
720+
412721
if (this.windows.has(windowId)) {
413722
this.closeWindow(windowId);
414723
} else {
@@ -617,8 +926,9 @@ const iconPaths = {
617926
}
618927
}
619928

620-
// Initialize window manager when DOM is ready
929+
// Initialize managers when DOM is ready
621930
document.addEventListener('DOMContentLoaded', () => {
931+
window.themeManager = new ThemeManager();
622932
new WindowManager();
623933

624934
// Initialize clock

0 commit comments

Comments
 (0)