Theme Rendering Fixes and Keybinds Update

This commit is contained in:
2025-11-28 21:11:45 +00:00
parent e7fa0547b7
commit 22e02eb97b
9 changed files with 206 additions and 45 deletions

View File

@@ -208,7 +208,7 @@
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 175px;
min-width: 200px;
background: var(--bar-bg);
border: 1px solid var(--bar-border);
border-radius: 8px;

View File

@@ -201,14 +201,16 @@
transition:fly={{ y: -10, duration: 150 }}
>
<div class="dropdown-header">Theme</div>
{#each themeOptions as option}
{#each themeOptions as option, i}
<button
class="theme-option"
class:active={$colorTheme === option.value}
onclick={() => handleThemeSelect(option.value)}
>
<Icon icon={getThemeIcon(option.value)} width="16" />
<span>{option.label}</span>
<span class="theme-label">
{option.label} <span class="theme-number">[{i+1}]</span>
</span>
{#if $colorTheme === option.value}
<Icon icon="mdi:check" width="14" class="check" />
{/if}
@@ -218,7 +220,7 @@
<div class="dropdown-header">Mode</div>
<button class="theme-option" onclick={toggleMode}>
<Icon icon={$mode === 'dark' ? 'mdi:weather-sunny' : 'mdi:weather-night'} width="16" />
<span>{$mode === 'dark' ? 'Light Mode' : 'Dark Mode'} (T)</span>
<span>{$mode === 'dark' ? 'Light Mode' : 'Dark Mode'} [T]</span>
</button>
</div>
{/if}
@@ -313,14 +315,15 @@
<div class="mobile-theme-section">
<div class="mobile-section-header">Theme</div>
<div class="mobile-theme-options">
{#each themeOptions as option}
{#each themeOptions as option, i}
<button
class="mobile-theme-btn"
class:active={$colorTheme === option.value}
onclick={() => { handleThemeSelect(option.value); closeMobileMenu(); }}
title={`Press T+${i+1} to switch to ${option.label}`}
>
<Icon icon={getThemeIcon(option.value)} width="18" />
<span>{option.label}</span>
<span class="theme-label"><span class="theme-number">{i+1}.</span> {option.label}</span>
</button>
{/each}
</div>

View File

@@ -61,6 +61,31 @@
let terminalElement: HTMLDivElement;
let bodyElement = $state<HTMLDivElement>();
// Track colorMap identity to detect theme/mode changes
let lastColorMapId = $state('');
// When colorMap changes (theme/mode toggle), update displayedLines with new parsed segments
$effect(() => {
// Create a simple identity string from a few colorMap values
const colorMapId = `${colorMap.red}-${colorMap.text}-${colorMap.primary}`;
// Only update if colorMap actually changed and animation is complete
if (colorMapId !== lastColorMapId && isComplete && displayedLines.length > 0) {
// Store current showImage states before updating
const showImageStates = displayedLines.map(d => d.showImage);
// Update with new parsed content
displayedLines = parsedLines.map((parsed, i) => ({
parsed,
charIndex: parsed.plainText.length,
complete: true,
showImage: showImageStates[i] ?? (parsed.line.type === 'image')
}));
}
lastColorMapId = colorMapId;
});
// Autoscroll to bottom (respects autoscroll prop)
function scrollToBottom() {
if (!autoscroll || !bodyElement) return;
@@ -251,12 +276,6 @@
}
function handleKeydown(event: KeyboardEvent) {
// Toggle theme with T key
if (event.key === 't' || event.key === 'T') {
event.preventDefault();
toggleMode();
return;
}
if (isTyping && (event.key === 'y' || event.key === 'Y')) {
event.preventDefault();
skipAnimation();

View File

@@ -21,7 +21,7 @@ export interface TextSegment {
// Color maps for each theme
export type ThemeColorMap = Record<string, string>;
// Default color map (fallback) - includes theme colors for backwards compatibility
// Default color map (fallback) - hex values as defaults, overwritten by theme colorMap at runtime
export const defaultColorMap: ThemeColorMap = {
// Basic colors
'red': '#f38ba8',
@@ -36,7 +36,7 @@ export const defaultColorMap: ThemeColorMap = {
'pink': '#f5c2e7',
'black': '#1e1e2e',
'surface': '#313244',
// Catppuccin extended colors
// Extended colors
'teal': '#94e2d5',
'sky': '#89dceb',
'sapphire': '#74c7ec',
@@ -47,14 +47,14 @@ export const defaultColorMap: ThemeColorMap = {
'flamingo': '#f2cdcd',
'rosewater': '#f5e0dc',
// Semantic colors
'primary': 'var(--terminal-primary)',
'accent': 'var(--terminal-accent)',
'muted': 'var(--terminal-muted)',
'primary': '#cba6f7',
'accent': '#a6e3a1',
'muted': '#6c7086',
'error': '#f38ba8',
'success': '#a6e3a1',
'warning': '#f9e2af',
'info': '#89b4fa',
// Theme colors (for using text, textMuted, etc. in formatter)
// Theme colors
'text': '#cdd6f4',
'textMuted': '#a6adc8',
'background': '#1e1e2e',

113
src/lib/keybinds.ts Normal file
View File

@@ -0,0 +1,113 @@
import hotkeys from 'hotkeys-js';
export interface KeybindsOptions {
themeOptions: Array<{ value: string }>;
setColorTheme: (t: any) => void;
toggleMode: () => void;
navigation: Array<{ path: string; external?: boolean }>;
goto?: (path: string) => void;
browser: boolean;
}
let lastToggleAt = 0;
let tHeld = false;
let tDigitUsed = false;
export function registerKeybinds(opts: KeybindsOptions) {
const { themeOptions, setColorTheme, toggleMode, navigation, goto, browser } = opts;
// alt/option + t: toggle mode (hold+digit to pick theme)
// keydown: start hold
hotkeys('alt+t', (e) => {
if (e.repeat) return;
tHeld = true;
tDigitUsed = false;
e.preventDefault();
});
// macOS alias: option
hotkeys('option+t', (e) => {
if (e.repeat) return;
tHeld = true;
tDigitUsed = false;
e.preventDefault();
});
// keyup: toggle if no digit used
hotkeys('alt+t', { keyup: true }, (e) => {
if (!tDigitUsed && tHeld) {
const now = Date.now();
if (now - lastToggleAt > 400) {
toggleMode();
lastToggleAt = now;
}
}
tHeld = false;
tDigitUsed = false;
e.preventDefault();
});
hotkeys('option+t', { keyup: true }, (e) => {
if (!tDigitUsed && tHeld) {
const now = Date.now();
if (now - lastToggleAt > 400) {
toggleMode();
lastToggleAt = now;
}
}
tHeld = false;
tDigitUsed = false;
e.preventDefault();
});
// alt/option + t + N => set theme
const tThemeKeysAlt = Array.from({ length: 9 }, (_, i) => `alt+t+${i + 1}`).join(',');
const tThemeKeysOption = Array.from({ length: 9 }, (_, i) => `option+t+${i + 1}`).join(',');
hotkeys(`${tThemeKeysAlt},${tThemeKeysOption}`, (e, handler) => {
const m = handler.key.match(/(?:alt|option)\+t\+(\d)/);
if (!m) return;
const idx = parseInt(m[1], 10) - 1;
if (themeOptions[idx]) {
setColorTheme(themeOptions[idx].value);
tDigitUsed = true;
e.preventDefault();
}
});
// ctrl/cmd + N => navigate. Include numpad variants. Command (cmd) aliases on mac
const ctrlKeys = Array.from({ length: 9 }, (_, i) => `ctrl+${i + 1}`).join(',');
const ctrlNumKeys = Array.from({ length: 9 }, (_, i) => `ctrl+num_${i + 1}`).join(',');
const cmdKeys = Array.from({ length: 9 }, (_, i) => `cmd+${i + 1}`).join(',');
const cmdNumKeys = Array.from({ length: 9 }, (_, i) => `cmd+num_${i + 1}`).join(',');
hotkeys(`${ctrlKeys},${ctrlNumKeys},${cmdKeys},${cmdNumKeys}`, (e, handler) => {
const digitMatch = handler.key.match(/(\d)/);
if (!digitMatch) return;
const digit = parseInt(digitMatch[1], 10);
if (isNaN(digit)) return;
const idx = digit - 1;
if (navigation && navigation[idx]) {
const { path, external } = navigation[idx] as any;
if (browser) {
if (external) window.location.href = path;
else if (typeof goto === 'function') goto?.(path);
}
e.preventDefault();
}
});
}
export function unregisterKeybinds() {
// Unbind all key patterns we registered
hotkeys.unbind('alt+t');
hotkeys.unbind('option+t');
const tThemeKeys = Array.from({ length: 9 }, (_, i) => `alt+t+${i + 1}`).join(',');
const tThemeKeysOption = Array.from({ length: 9 }, (_, i) => `option+t+${i + 1}`).join(',');
hotkeys.unbind(`${tThemeKeys},${tThemeKeysOption}`);
const ctrlKeys = Array.from({ length: 9 }, (_, i) => `ctrl+${i + 1}`).join(',');
const ctrlNumKeys = Array.from({ length: 9 }, (_, i) => `ctrl+num_${i + 1}`).join(',');
const cmdKeys = Array.from({ length: 9 }, (_, i) => `cmd+${i + 1}`).join(',');
const cmdNumKeys = Array.from({ length: 9 }, (_, i) => `cmd+num_${i + 1}`).join(',');
hotkeys.unbind(`${ctrlKeys},${ctrlNumKeys},${cmdKeys},${cmdNumKeys}`);
}

View File

@@ -1,6 +1,7 @@
import type { TerminalLine } from "$lib/components/tui/types";
import { user } from "$lib/config";
import { navigation } from "$lib/config";
import { themeOptions } from "$lib/stores/theme";
export const lines: TerminalLine[] = [
// neofetch style intro
@@ -14,7 +15,8 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' },
{ type: 'header', content: `Welcome to ${user.displayname}'s Portfolio` },
{ type: 'blank', content: '' },
{ type: 'output', content: `(&muted)${user.bio}(&)` },
// { type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: false },
{ type: 'output', content: `(&muted)${user.bio}(&)`, inline: true },
{ type: 'blank', content: '' },
{ type: 'divider', content: 'NAVIGATION' },
@@ -32,10 +34,16 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' },
{ type: 'divider', content: 'Website Keybinds' },
{ type: 'blank', content: '' },
{ type: 'output', content: '(&orange)Toggle Light/Dark Mode(&) (&text, bold)(T)(&)' , inline: true },
{ type: 'output', content: '(&orange)Toggle Light/Dark Mode(&) (&text, bold)(Alt/Option+T)(&)' , inline: true },
{ type: 'output', content: '(&muted)•(&)' , inline: true },
{ type: 'output', content: '(&orange)Select Theme(&) (&text, bold)Use Alt/Option+T + [#](&)' , inline: true },
{ type: 'output', content: '(&muted)•(&)' , inline: true },
{ type: 'output', content: '(&orange)Skip typing animation(&) (&text, bold)(Y)(&)' , inline: true },
{ type: 'output', content: '(&muted)•(&)' , inline: true },
{ type: 'output', content: '(&orange)Open/Close Navbar(&) (&text, bold)(N)(&)' , inline: true },
{ type: 'output', content: '(&orange)Navigate to page(&) (&text, bold)(Ctrl/Cmd+ [#])(&)' , inline: true },
{ type: 'blank', content: '' },
// list navigation with numeric shortcuts
{ type: 'output', content: '(&text, bold)Pages -(&)', inline: true },
...navigation.map((nav, i) => ({ type: 'output' as const, content: `(&blue)${nav.name}(&) (&text, bold)[${i+1}](&)`, inline: true })),
{ type: 'blank', content: '' },
];

View File

@@ -48,30 +48,27 @@ const themes: Record<ColorTheme, ThemeJson> = {
// Export themes for external access
export { themes };
// CSS variable mappings for theme colors - these update dynamically with mode changes
const themeColorVars: Record<string, string> = {
'primary': 'var(--terminal-primary)',
'secondary': 'var(--terminal-secondary)',
'accent': 'var(--terminal-accent)',
'background': 'var(--terminal-bg)',
'backgroundLight': 'var(--terminal-bg-light)',
'text': 'var(--terminal-text)',
'textMuted': 'var(--terminal-muted)',
'border': 'var(--terminal-border)',
'terminal': 'var(--terminal-bg)',
'terminalPrompt': 'var(--terminal-prompt)',
'terminalUser': 'var(--terminal-user)',
'terminalPath': 'var(--terminal-path)',
};
// Build theme colors from JSON - merges colors into colorMap so formatter can use both
function buildThemeColors(theme: ThemeJson, mode: Mode): ThemeColors {
const modeData = theme[mode];
// Merge colors and colorMap - use CSS variables for theme colors so they update dynamically
// Merge: defaults -> theme palette -> theme colors (so text, primary, etc. are correct hex values)
const mergedColorMap: ThemeColorMap = {
...defaultColorMap, // Fallback defaults
...(modeData.colorMap ?? {}), // Theme's color palette
...themeColorVars // Theme colors as CSS variables - update with mode changes
...defaultColorMap, // Fallback defaults (hex values)
...(modeData.colorMap ?? {}), // Theme's color palette (hex values)
// Also add theme colors so (&text), (&primary), etc. resolve to correct hex for this mode
'text': modeData.colors.text,
'textMuted': modeData.colors.textMuted,
'primary': modeData.colors.primary,
'secondary': modeData.colors.secondary,
'accent': modeData.colors.accent,
'background': modeData.colors.background,
'backgroundLight': modeData.colors.backgroundLight,
'border': modeData.colors.border,
'terminal': modeData.colors.terminal,
'terminalPrompt': modeData.colors.terminalPrompt,
'terminalUser': modeData.colors.terminalUser,
'terminalPath': modeData.colors.terminalPath,
'muted': modeData.colors.textMuted, // alias
};
return {
primary: modeData.colors.primary,

View File

@@ -1,17 +1,19 @@
<script lang="ts">
import './layout.css';
import Navbar from '$lib/components/Navbar.svelte';
import Background3D from '$lib/components/Background3D.svelte';
import { themeColors, mode, colorTheme } from '$lib/stores/theme';
import { themeColors, mode, colorTheme, toggleMode, setColorTheme, themeOptions } from '$lib/stores/theme';
import { goto } from '$app/navigation';
import { navigation } from '$lib/config';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { registerKeybinds, unregisterKeybinds } from '$lib/keybinds';
import NavbarWaybar from '$lib/components/NavbarWaybar.svelte';
let { children } = $props();
let mounted = $state(false);
// don't bind the component; find the DOM element by its class
function updateNavbarHeight() {
if (!browser) return;
const elem = document.querySelector('.navbar') as HTMLElement | null;
if (!elem) return;
const rect = elem.getBoundingClientRect();
@@ -28,6 +30,24 @@
// Update CSS var with actual navbar height
updateNavbarHeight();
window.addEventListener('resize', updateNavbarHeight);
// Register centralized keybinds
registerKeybinds({
themeOptions: themeOptions,
setColorTheme: setColorTheme,
toggleMode: toggleMode,
navigation: navigation,
goto: goto,
browser: browser
});
});
onDestroy(() => {
if (browser) {
window.removeEventListener('resize', updateNavbarHeight);
// Unregister centralized keybinds
unregisterKeybinds();
}
});
$effect(() => updateNavbarHeight());