Files
kennethreitz.org/templates/base.html
T

781 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}kennethreitz.org — {% endblock %}</title>
<!-- Import fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<!-- CSS -->
<link rel="stylesheet" href="/static/custom.css">
<!-- Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💻</text></svg>">
<!-- Prism.js for Syntax Highlighting - Using VS Code-inspired Theme -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/plugins/line-numbers/prism-line-numbers.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/prism.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/plugins/line-numbers/prism-line-numbers.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/plugins/toolbar/prism-toolbar.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-python.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-javascript.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-css.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-bash.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-json.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-markdown.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-yaml.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-jsx.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-typescript.min.js" defer></script>
{% block head %}{% endblock %}
</head>
<body class="bg-background dark:bg-background-dark text-text dark:text-text-light">
<header id="site-header">
<div class="container">
<h2 class="site-title"><a href="/">kennethreitz.org</a></h2>
<nav>
<ul>
<li><a href="/software" {% if '/software' in request.path %}class="active"{% endif %}>Software</a></li>
<li><a href="/essays" {% if '/essays' in request.path %}class="active"{% endif %}>Essays</a></li>
<li><a href="/talks" {% if '/talks' in request.path %}class="active"{% endif %}>Talks</a></li>
<li><a href="/music" {% if '/music' in request.path %}class="active"{% endif %}>Music</a></li>
<li><a href="/poetry" {% if '/poetry' in request.path %}class="active"{% endif %}>Poetry</a></li>
<li><a href="/artificial-intelligence" {% if '/artificial-intelligence' in request.path %}class="active"{% endif %}>AI</a></li>
<li><a href="/contact" {% if '/contact' in request.path %}class="active"{% endif %}>Contact</a></li>
</ul>
</nav>
</div>
</header>
<main class="py-8">
<div class="container">
<div class="content-narrow">
{% block content %}{% endblock %}
</div>
</div>
</main>
<!-- VS Code-inspired Explorer Button -->
<div id="explorer-button" class="explorer-fab">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3h18v18H3zM9 3v18M3 9h6M3 15h6"></path>
</svg>
<span class="shortcut-hint">Ctrl+E</span>
</div>
<!-- Explorer Panel -->
<div id="explorer-panel" class="explorer-panel">
<div class="explorer-header">
<h3>EXPLORER</h3>
<button id="explorer-close">×</button>
</div>
<div class="explorer-body">
<div class="explorer-section">
<div class="explorer-section-header">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
</svg>
<span>PROJECTS</span>
</div>
<div class="explorer-section-content">
<a href="/software/requests" class="explorer-item">requests</a>
<a href="/software/pipenv" class="explorer-item">pipenv</a>
<a href="/software/tablib" class="explorer-item">tablib</a>
<a href="/software/responder" class="explorer-item">responder</a>
</div>
</div>
<div class="explorer-section">
<div class="explorer-section-header">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
<span>POSTS</span>
</div>
<div class="explorer-section-content">
<a href="/essays" class="explorer-item">Essays</a>
<a href="/poetry" class="explorer-item">Poetry</a>
<a href="/artificial-intelligence" class="explorer-item">AI</a>
</div>
</div>
<div class="explorer-section">
<div class="explorer-section-header">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 8l4 4-4 4M8 12h8"></path>
</svg>
<span>LINKS</span>
</div>
<div class="explorer-section-content">
<a href="https://github.com/kennethreitz" class="explorer-item external">GitHub</a>
<a href="https://twitter.com/kennethreitz42" class="explorer-item external">Twitter</a>
<a href="https://www.linkedin.com/in/kennethreitz/" class="explorer-item external">LinkedIn</a>
</div>
</div>
</div>
</div>
<!-- Command Palette -->
<div id="command-palette" class="command-palette">
<div class="command-palette-header">
<input type="text" class="command-palette-search" id="command-search" placeholder="Type a command or search..." autocomplete="off">
</div>
<div class="command-palette-results" id="command-results">
<!-- Results will be populated dynamically -->
</div>
</div>
<!-- Keyboard Shortcuts Panel -->
<div id="shortcuts-panel" class="shortcuts-panel">
<div class="shortcuts-header">
<h3 class="shortcuts-title">Keyboard Shortcuts</h3>
<button id="shortcuts-close" class="shortcuts-close">×</button>
</div>
<div class="shortcuts-content">
<div class="shortcuts-section">
<h4 class="shortcuts-section-title">Navigation</h4>
<div class="shortcuts-list">
<div class="shortcut-item">
<span class="shortcut-label">Open Explorer</span>
<div class="shortcut-combo">
<span class="shortcut-key">Ctrl</span>
<span class="shortcut-key">E</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Command Palette</span>
<div class="shortcut-combo">
<span class="shortcut-key">Ctrl</span>
<span class="shortcut-key">P</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Go Home</span>
<div class="shortcut-combo">
<span class="shortcut-key">Alt</span>
<span class="shortcut-key">Home</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Keyboard Shortcuts</span>
<div class="shortcut-combo">
<span class="shortcut-key">?</span>
</div>
</div>
</div>
</div>
<div class="shortcuts-section">
<h4 class="shortcuts-section-title">Content</h4>
<div class="shortcuts-list">
<div class="shortcut-item">
<span class="shortcut-label">Copy Code</span>
<div class="shortcut-combo">
<span class="shortcut-key">Alt</span>
<span class="shortcut-key">C</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Toggle Dark Mode</span>
<div class="shortcut-combo">
<span class="shortcut-key">Alt</span>
<span class="shortcut-key">T</span>
</div>
</div>
</div>
</div>
<div class="shortcuts-section">
<h4 class="shortcuts-section-title">Quick Access</h4>
<div class="shortcuts-list">
<div class="shortcut-item">
<span class="shortcut-label">Software</span>
<div class="shortcut-combo">
<span class="shortcut-key">G</span>
<span class="shortcut-key">S</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Essays</span>
<div class="shortcut-combo">
<span class="shortcut-key">G</span>
<span class="shortcut-key">E</span>
</div>
</div>
<div class="shortcut-item">
<span class="shortcut-label">Contact</span>
<div class="shortcut-combo">
<span class="shortcut-key">G</span>
<span class="shortcut-key">C</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Overlay for modals -->
<div id="shortcuts-overlay" class="shortcuts-overlay"></div>
<footer class="mt-auto py-10 bg-primary-light/50 dark:bg-primary-dark/10 border-t border-border dark:border-border-dark">
<div class="container flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex gap-6">
<a href="/software" class="text-text dark:text-text-light no-underline hover:text-primary transition-colors">Software</a>
<a href="/essays" class="text-text dark:text-text-light no-underline hover:text-primary transition-colors">Essays</a>
<a href="/talks" class="text-text dark:text-text-light no-underline hover:text-primary transition-colors">Talks</a>
<a href="/values" class="text-text dark:text-text-light no-underline hover:text-primary transition-colors">Values</a>
<a href="/contact" class="text-text dark:text-text-light no-underline hover:text-primary transition-colors">Contact</a>
</div>
<div class="text-center p-4 bg-white/10 dark:bg-white/5 rounded-lg max-w-[600px] mx-auto">
<p id="kenneth-quote" class="italic relative cursor-pointer inline-block px-2 transition-all">"Attention is the only currency we have in life. Thanks for gifting me yours."</p>
</div>
<div class="flex items-center gap-4">
<div id="shortcuts-indicator" class="cursor-pointer text-sm flex items-center gap-1 py-1 px-2 rounded bg-white/10 dark:bg-white/5 text-text-secondary dark:text-text-light/60 font-mono hover:bg-white/20 dark:hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span>Press ? for shortcuts</span>
</div>
<div id="elevenlabs-audionative-widget" data-height="90" data-width="100%" data-frameborder="no" data-scrolling="no" data-publicuserid="09af4c720273252ac5bcddb53500ed22b7c59ef815a710a03ea48b18e295c24f" data-playerurl="https://elevenlabs.io/player/index.html" class="mt-6 w-full rounded overflow-hidden">Loading the <a href="https://elevenlabs.io/text-to-speech" target="_blank" rel="noopener">Elevenlabs Text to Speech</a> AudioNative Player...</div>
<script src="https://elevenlabs.io/player/audioNativeHelper.js" type="text/javascript"></script>
</div>
</div>
</footer>
<script>
// Handle header scroll effect
document.addEventListener('DOMContentLoaded', function() {
const header = document.getElementById('site-header');
const scrollWatcher = () => {
if (window.scrollY > 10) {
header.classList.add('shadow-sm');
} else {
header.classList.remove('shadow-sm');
}
};
window.addEventListener('scroll', scrollWatcher);
// IDE-style initialization animation effect - only on first visit
const mainContent = document.querySelector('main');
if (mainContent) {
// Check if this is the user's first visit to the site in this session
const hasVisited = sessionStorage.getItem('has_visited_site');
if (!hasVisited) {
// Set the flag that the user has visited
sessionStorage.setItem('has_visited_site', 'true');
// Add terminal loading animation
const loadingTerminal = document.createElement('div');
loadingTerminal.className = 'terminal-loader';
loadingTerminal.innerHTML = `
<div class="terminal-header">
<span class="terminal-title">initializing kennethreitz.org</span>
</div>
<div class="terminal-content" id="terminal-content">
<p class="command-line">Loading environment variables...</p>
<p class="command-output">✓ Environment configured</p>
<p class="command-line">Setting up developer experience...</p>
<p class="command-output">✓ Dependencies installed</p>
<p class="command-line">Applying syntax highlighting...</p>
<p class="command-output">✓ Prism.js activated</p>
<p class="command-line">Initializing code editor interface...</p>
<p class="command-output">✓ Visual improvements applied</p>
<p class="command-line">Rendering content...</p>
</div>
`;
document.body.appendChild(loadingTerminal);
// Simulate typing effect
setTimeout(() => {
const finalLine = document.createElement('p');
finalLine.className = 'command-output success';
finalLine.textContent = '✓ kennethreitz.org loaded successfully!';
document.getElementById('terminal-content').appendChild(finalLine);
setTimeout(() => {
loadingTerminal.classList.add('fade-out');
setTimeout(() => {
loadingTerminal.remove();
mainContent.classList.add('animate-in');
}, 500);
}, 1000);
}, 800);
} else {
// If not first visit, just add the animation class
mainContent.classList.add('animate-in');
}
}
// Kenneth's quotes rotation
const kennethQuotes = [
"Attention is the only currency we have in life. Thanks for gifting me yours.",
"Be cordial or be on your way.",
"Reality exists regardless of your emotional support of it.",
"Everything is an expression of its opposite.",
"Documentation is king.",
"Trust and verify.",
"I develop things for humans.",
"Optimize for simplicity, then performance.",
"Python for humans.",
"The power of a clean API.",
"Clarity over cleverness.",
"Life is not a race, but there's no speed limit either."
];
const quoteElement = document.getElementById('kenneth-quote');
if (quoteElement) {
// Set initial quote to avoid flicker
const initialQuote = quoteElement.textContent;
// Filter out the initial quote
const filteredQuotes = kennethQuotes.filter(quote =>
quote !== initialQuote.replace(/^"|"$/g, ''));
// Show a random quote on page reload
const randomIndex = Math.floor(Math.random() * filteredQuotes.length);
quoteElement.textContent = `"${filteredQuotes[randomIndex]}"`;
// Add hover effect for quote change
quoteElement.addEventListener('mouseover', function() {
const randomIndex = Math.floor(Math.random() * kennethQuotes.length);
this.classList.add('opacity-0');
setTimeout(() => {
this.textContent = `"${kennethQuotes[randomIndex]}"`;
this.classList.remove('opacity-0');
}, 300);
});
}
// Animate directory items
const directoryItems = document.querySelectorAll('.directory-item');
if (directoryItems.length) {
directoryItems.forEach((item, index) => {
item.classList.add('opacity-0');
setTimeout(() => {
item.classList.remove('opacity-0');
item.classList.add('transition-opacity', 'duration-500');
}, index * 50);
});
}
// Add copy buttons to code blocks and enhance them
function setupCodeBlocks() {
// Wait for Prism.js to finish highlighting
setTimeout(() => {
const codeBlocks = document.querySelectorAll('pre:not(.copied-button-added)');
codeBlocks.forEach(pre => {
// Mark as processed
pre.classList.add('copied-button-added');
// Add line numbers to all code blocks
pre.classList.add('line-numbers');
// Create copy button
const copyButton = document.createElement('button');
copyButton.className = 'copy-code-button';
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy';
// Add language tag if possible
const code = pre.querySelector('code');
if (code && code.className) {
const match = code.className.match(/language-([a-z]+)/);
if (match && match[1]) {
pre.setAttribute('data-language', match[1]);
// Add a title attribute with full language name
const languageMap = {
'py': 'Python',
'js': 'JavaScript',
'jsx': 'React JSX',
'ts': 'TypeScript',
'json': 'JSON',
'css': 'CSS',
'html': 'HTML',
'bash': 'Bash',
'shell': 'Shell',
'md': 'Markdown',
'yaml': 'YAML',
'yml': 'YAML'
};
const langName = languageMap[match[1]] || match[1].charAt(0).toUpperCase() + match[1].slice(1);
pre.setAttribute('data-language', langName);
}
}
// Add copy functionality
copyButton.addEventListener('click', () => {
const code = pre.querySelector('code');
const textToCopy = code.textContent;
// Copy to clipboard
navigator.clipboard.writeText(textToCopy).then(() => {
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"></path></svg> Copied!';
copyButton.style.backgroundColor = 'rgba(var(--color-success), 0.2)';
copyButton.style.borderColor = 'rgba(var(--color-success), 0.5)';
setTimeout(() => {
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy';
copyButton.style.backgroundColor = '';
copyButton.style.borderColor = '';
}, 2000);
})
.catch(err => {
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> Error';
copyButton.style.backgroundColor = 'rgba(var(--color-error), 0.2)';
copyButton.style.borderColor = 'rgba(var(--color-error), 0.5)';
setTimeout(() => {
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy';
copyButton.style.backgroundColor = '';
copyButton.style.borderColor = '';
}, 2000);
console.error('Failed to copy code: ', err);
});
});
pre.appendChild(copyButton);
// Add editor info bar
const infoBar = document.createElement('div');
infoBar.className = 'code-editor-info';
infoBar.innerHTML = `
<div class="editor-tabs">
<div class="editor-tab active">
${pre.getAttribute('data-language') || 'Code'}
<span class="close-icon">×</span>
</div>
</div>
`;
pre.parentNode.insertBefore(infoBar, pre);
// Wrap pre in a container for styling
const wrapper = document.createElement('div');
wrapper.className = 'code-editor-wrapper';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
});
}, 500); // Wait for Prism.js to initialize
}
// Initial setup
setupCodeBlocks();
// Re-run setup when content changes (for dynamic content)
const observer = new MutationObserver(setupCodeBlocks);
observer.observe(document.body, {
childList: true,
subtree: true
});
// Explorer panel functionality
const explorerButton = document.getElementById('explorer-button');
const explorerPanel = document.getElementById('explorer-panel');
const explorerClose = document.getElementById('explorer-close');
if (explorerButton && explorerPanel && explorerClose) {
// Open explorer panel
explorerButton.addEventListener('click', () => {
explorerPanel.classList.add('show');
});
// Close explorer panel
explorerClose.addEventListener('click', () => {
explorerPanel.classList.remove('show');
});
// Close panel when clicking outside
document.addEventListener('click', (e) => {
if (!explorerPanel.contains(e.target) && e.target !== explorerButton) {
explorerPanel.classList.remove('show');
}
});
// Handle section toggle
const sectionHeaders = document.querySelectorAll('.explorer-section-header');
sectionHeaders.forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
if (content.style.display === 'none') {
content.style.display = 'block';
} else {
content.style.display = 'none';
}
});
});
}
// Command palette setup
const commandPalette = document.getElementById('command-palette');
const commandSearch = document.getElementById('command-search');
const commandResults = document.getElementById('command-results');
const shortcutsPanel = document.getElementById('shortcuts-panel');
const shortcutsClose = document.getElementById('shortcuts-close');
const shortcutsOverlay = document.getElementById('shortcuts-overlay');
// Define available commands
const commands = [
{
id: 'explorer',
label: 'Toggle Explorer',
shortcut: 'Ctrl+E',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h18v18H3zM9 3v18M3 9h6M3 15h6"></path></svg>',
action: () => toggleExplorer()
},
{
id: 'home',
label: 'Go to Home',
shortcut: 'Alt+Home',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>',
action: () => window.location.href = '/'
},
{
id: 'software',
label: 'Software Projects',
shortcut: 'G S',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8L16 3H8z"></path><path d="M17 21v-8H7v8M7 3v5h8"></path></svg>',
action: () => window.location.href = '/software'
},
{
id: 'essays',
label: 'Essays',
shortcut: 'G E',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>',
action: () => window.location.href = '/essays'
},
{
id: 'contact',
label: 'Contact',
shortcut: 'G C',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>',
action: () => window.location.href = '/contact'
},
{
id: 'shortcuts',
label: 'Keyboard Shortcuts',
shortcut: '?',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
action: () => toggleShortcutsPanel()
}
];
// Function to render commands in palette
function renderCommands(filterText = '') {
commandResults.innerHTML = '';
const filteredCommands = filterText
? commands.filter(cmd =>
cmd.label.toLowerCase().includes(filterText.toLowerCase()) ||
cmd.id.toLowerCase().includes(filterText.toLowerCase()))
: commands;
if (filteredCommands.length === 0) {
const noResults = document.createElement('div');
noResults.className = 'command-palette-item';
noResults.innerHTML = `
<div class="command-palette-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<span class="command-palette-label">No matching commands found</span>
`;
commandResults.appendChild(noResults);
return;
}
filteredCommands.forEach((cmd, index) => {
const item = document.createElement('div');
item.className = 'command-palette-item';
if (index === 0) item.classList.add('selected');
item.innerHTML = `
<div class="command-palette-icon">${cmd.icon}</div>
<span class="command-palette-label">${cmd.label}</span>
<div class="command-palette-shortcut">
${cmd.shortcut.split(' ').map(key =>
`<span class="command-palette-key">${key}</span>`
).join('')}
</div>
`;
item.addEventListener('click', () => {
cmd.action();
toggleCommandPalette(false);
});
commandResults.appendChild(item);
});
}
// Toggle command palette
function toggleCommandPalette(show = true) {
if (show) {
commandPalette.classList.add('show');
shortcutsOverlay.classList.add('show');
commandSearch.value = '';
renderCommands();
setTimeout(() => commandSearch.focus(), 100);
} else {
commandPalette.classList.remove('show');
shortcutsOverlay.classList.remove('show');
}
}
// Toggle shortcuts panel
function toggleShortcutsPanel(show = true) {
if (show) {
shortcutsPanel.classList.add('show');
shortcutsOverlay.classList.add('show');
} else {
shortcutsPanel.classList.remove('show');
shortcutsOverlay.classList.remove('show');
}
}
// Toggle explorer
function toggleExplorer(show) {
if (show === undefined) {
explorerPanel.classList.toggle('show');
} else if (show) {
explorerPanel.classList.add('show');
} else {
explorerPanel.classList.remove('show');
}
}
// Command palette search functionality
if (commandSearch) {
commandSearch.addEventListener('input', (e) => {
renderCommands(e.target.value);
});
commandSearch.addEventListener('keydown', (e) => {
const items = commandResults.querySelectorAll('.command-palette-item');
const selected = commandResults.querySelector('.selected');
let index = Array.from(items).indexOf(selected);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (index < items.length - 1) {
if (selected) selected.classList.remove('selected');
items[index + 1].classList.add('selected');
items[index + 1].scrollIntoView({ block: 'nearest' });
}
break;
case 'ArrowUp':
e.preventDefault();
if (index > 0) {
if (selected) selected.classList.remove('selected');
items[index - 1].classList.add('selected');
items[index - 1].scrollIntoView({ block: 'nearest' });
}
break;
case 'Enter':
e.preventDefault();
if (selected) {
selected.click();
}
break;
case 'Escape':
e.preventDefault();
toggleCommandPalette(false);
break;
}
});
}
// Close shortcuts panel
if (shortcutsClose) {
shortcutsClose.addEventListener('click', () => {
toggleShortcutsPanel(false);
});
}
// Close modals when clicking on overlay
if (shortcutsOverlay) {
shortcutsOverlay.addEventListener('click', () => {
toggleCommandPalette(false);
toggleShortcutsPanel(false);
});
}
// Handle shortcuts indicator click
const shortcutsIndicator = document.getElementById('shortcuts-indicator');
if (shortcutsIndicator) {
shortcutsIndicator.addEventListener('click', () => {
toggleShortcutsPanel(true);
});
}
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Toggle explorer (Ctrl+E or Cmd+E)
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
e.preventDefault();
toggleExplorer();
}
// Command palette (Ctrl+P or Cmd+P)
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
e.preventDefault();
toggleCommandPalette(true);
}
// Shortcuts panel (?)
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey &&
!(document.activeElement instanceof HTMLInputElement) &&
!(document.activeElement instanceof HTMLTextAreaElement)) {
e.preventDefault();
toggleShortcutsPanel(true);
}
// Quick navigation with g prefix
if (e.key === 'g' && !shortcutsPanel.classList.contains('show') &&
!commandPalette.classList.contains('show') &&
!(document.activeElement instanceof HTMLInputElement) &&
!(document.activeElement instanceof HTMLTextAreaElement)) {
const handleSecondKey = (e2) => {
document.removeEventListener('keydown', handleSecondKey);
if (e2.key === 's') {
window.location.href = '/software';
} else if (e2.key === 'e') {
window.location.href = '/essays';
} else if (e2.key === 'c') {
window.location.href = '/contact';
}
};
document.addEventListener('keydown', handleSecondKey);
}
// Home shortcut (Alt+Home)
if (e.altKey && e.key === 'Home') {
e.preventDefault();
window.location.href = '/';
}
});
});
</script>
</body>
</html>