Add keyboard shortcuts and command palette

This commit is contained in:
2025-04-22 16:29:43 -04:00
parent 874025af4f
commit fa6ba73be0
2 changed files with 694 additions and 8 deletions
+329
View File
@@ -1637,6 +1637,28 @@ pre[class*="language-"] {
transform: translateY(-1px);
}
.explorer-fab .shortcut-hint {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
background-color: rgb(var(--color-background-code));
color: rgb(var(--color-text-secondary));
padding: 3px 6px;
border-radius: 4px;
font-size: 0.7rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
white-space: nowrap;
opacity: 0;
transition: opacity 0.2s ease;
border: 1px solid rgb(var(--color-border));
box-shadow: var(--shadow-sm);
}
.explorer-fab:hover .shortcut-hint {
opacity: 1;
}
.explorer-panel {
position: fixed;
top: 0;
@@ -1755,6 +1777,12 @@ pre[class*="language-"] {
background-color: rgb(var(--color-primary));
}
.explorer-fab .shortcut-hint {
background-color: rgb(var(--color-background-code-dark));
color: rgb(var(--color-text-light) / 0.7);
border-color: rgb(var(--color-border-dark));
}
.explorer-panel {
background-color: rgb(var(--color-background-code-dark));
border-color: rgb(var(--color-border-dark));
@@ -1823,6 +1851,307 @@ pre[class*="language-"] {
}
}
/* Command Palette */
.command-palette {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
width: 600px;
max-width: 90vw;
max-height: 400px;
background-color: rgb(var(--color-background-code));
border-radius: 6px;
border: 1px solid rgb(var(--color-border));
box-shadow: var(--shadow-lg);
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
overflow: hidden;
display: flex;
flex-direction: column;
}
.command-palette.show {
opacity: 1;
pointer-events: all;
transform: translate(-50%, -50%) scale(1);
}
.command-palette-header {
padding: 10px 15px;
border-bottom: 1px solid rgb(var(--color-border));
display: flex;
align-items: center;
}
.command-palette-search {
flex: 1;
background: none;
border: none;
outline: none;
color: rgb(var(--color-text));
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.9rem;
}
.command-palette-search::placeholder {
color: rgb(var(--color-text-secondary) / 0.6);
}
.command-palette-results {
max-height: 350px;
overflow-y: auto;
padding: 5px 0;
}
.command-palette-item {
padding: 8px 15px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.command-palette-item:hover,
.command-palette-item.selected {
background-color: rgba(var(--color-primary), 0.1);
}
.command-palette-icon {
color: rgb(var(--color-text-secondary));
width: 16px;
flex-shrink: 0;
}
.command-palette-label {
flex: 1;
font-size: 0.9rem;
color: rgb(var(--color-text));
}
.command-palette-shortcut {
display: flex;
gap: 4px;
}
.command-palette-key {
font-size: 0.75rem;
color: rgb(var(--color-text-secondary));
background-color: rgba(var(--color-background), 0.8);
border: 1px solid rgb(var(--color-border));
border-radius: 3px;
padding: 1px 5px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
/* Keyboard Shortcuts Panel */
.shortcuts-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
width: 700px;
max-width: 90vw;
max-height: 80vh;
background-color: rgb(var(--color-background));
border-radius: 6px;
border: 1px solid rgb(var(--color-border));
box-shadow: var(--shadow-lg);
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
overflow: hidden;
display: flex;
flex-direction: column;
}
.shortcuts-panel.show {
opacity: 1;
pointer-events: all;
transform: translate(-50%, -50%) scale(1);
}
.shortcuts-header {
padding: 15px;
border-bottom: 1px solid rgb(var(--color-border));
display: flex;
align-items: center;
justify-content: space-between;
}
.shortcuts-title {
font-size: 1rem;
font-weight: 600;
margin: 0;
color: rgb(var(--color-text));
}
.shortcuts-close {
background: none;
border: none;
outline: none;
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
color: rgb(var(--color-text-secondary));
}
.shortcuts-content {
padding: 20px;
overflow-y: auto;
max-height: calc(80vh - 60px);
}
.shortcuts-section {
margin-bottom: 25px;
}
.shortcuts-section-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 10px;
color: rgb(var(--color-primary));
border-bottom: 1px solid rgba(var(--color-border), 0.3);
padding-bottom: 5px;
}
.shortcuts-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 10px;
}
.shortcut-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.shortcut-item:hover {
background-color: rgba(var(--color-primary), 0.05);
}
.shortcut-label {
font-size: 0.85rem;
color: rgb(var(--color-text));
}
.shortcut-combo {
display: flex;
gap: 4px;
}
.shortcut-key {
font-size: 0.75rem;
color: rgb(var(--color-text-secondary));
background-color: rgb(var(--color-background-code));
border: 1px solid rgb(var(--color-border));
border-radius: 3px;
padding: 2px 6px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
min-width: 20px;
text-align: center;
}
.shortcuts-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.shortcuts-overlay.show {
opacity: 1;
pointer-events: all;
}
@media (prefers-color-scheme: dark) {
.command-palette {
background-color: rgb(var(--color-background-code-dark));
border-color: rgb(var(--color-border-dark));
}
.command-palette-header {
border-color: rgb(var(--color-border-dark));
}
.command-palette-search {
color: rgb(var(--color-text-light));
}
.command-palette-search::placeholder {
color: rgb(var(--color-text-light) / 0.5);
}
.command-palette-item:hover,
.command-palette-item.selected {
background-color: rgba(var(--color-primary-dark), 0.3);
}
.command-palette-icon {
color: rgb(var(--color-text-light) / 0.7);
}
.command-palette-label {
color: rgb(var(--color-text-light));
}
.command-palette-key {
color: rgb(var(--color-text-light) / 0.7);
background-color: rgb(40, 44, 52);
border-color: rgb(var(--color-border-dark));
}
.shortcuts-panel {
background-color: rgb(var(--color-background-dark));
border-color: rgb(var(--color-border-dark));
}
.shortcuts-header {
border-color: rgb(var(--color-border-dark));
}
.shortcuts-title {
color: rgb(var(--color-text-light));
}
.shortcuts-close {
color: rgb(var(--color-text-light) / 0.7);
}
.shortcuts-section-title {
color: rgb(var(--color-accent-light));
border-color: rgba(var(--color-border-dark), 0.3);
}
.shortcut-item:hover {
background-color: rgba(var(--color-primary-dark), 0.2);
}
.shortcut-label {
color: rgb(var(--color-text-light));
}
.shortcut-key {
color: rgb(var(--color-text-light) / 0.8);
background-color: rgb(var(--color-background-code-dark));
border-color: rgb(var(--color-border-dark));
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
html {
+365 -8
View File
@@ -65,6 +65,7 @@
<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 -->
@@ -119,6 +120,106 @@
</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">
@@ -132,8 +233,17 @@
<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 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 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>
@@ -407,15 +517,262 @@
});
}
// Add keyboard shortcut (Ctrl+E or Cmd+E) to toggle explorer
// 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();
if (explorerPanel.classList.contains('show')) {
explorerPanel.classList.remove('show');
} else {
explorerPanel.classList.add('show');
}
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 = '/';
}
});
});