@@ -7,6 +7,14 @@ export interface Props {
77const { title, description = " Johan Wulf - Software Engineer" } = Astro .props
88const 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+
1018const 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
3751const 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