Theme Rendering Fixes and Keybinds Update
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
113
src/lib/keybinds.ts
Normal 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}`);
|
||||
}
|
||||
@@ -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: '' },
|
||||
];
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user