From 22e02eb97b48d421dabc3375497b773e43e20b45 Mon Sep 17 00:00:00 2001 From: Sir Blob Date: Fri, 28 Nov 2025 21:11:45 +0000 Subject: [PATCH] Theme Rendering Fixes and Keybinds Update --- package.json | 1 + src/lib/assets/css/navbar-waybar.css | 2 +- src/lib/components/NavbarWaybar.svelte | 13 +-- src/lib/components/TerminalTUI.svelte | 31 +++++-- src/lib/components/tui/utils.ts | 12 +-- src/lib/keybinds.ts | 113 +++++++++++++++++++++++++ src/lib/pages/home.ts | 14 ++- src/lib/stores/theme.ts | 37 ++++---- src/routes/+layout.svelte | 28 +++++- 9 files changed, 206 insertions(+), 45 deletions(-) create mode 100644 src/lib/keybinds.ts diff --git a/package.json b/package.json index e085909..dbae9a1 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "hotkeys-js": "^4.0.0-beta.7", "three": "^0.181.2" } } diff --git a/src/lib/assets/css/navbar-waybar.css b/src/lib/assets/css/navbar-waybar.css index a477e2d..fe033ab 100644 --- a/src/lib/assets/css/navbar-waybar.css +++ b/src/lib/assets/css/navbar-waybar.css @@ -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; diff --git a/src/lib/components/NavbarWaybar.svelte b/src/lib/components/NavbarWaybar.svelte index 71a160b..21379d7 100644 --- a/src/lib/components/NavbarWaybar.svelte +++ b/src/lib/components/NavbarWaybar.svelte @@ -201,14 +201,16 @@ transition:fly={{ y: -10, duration: 150 }} > - {#each themeOptions as option} + {#each themeOptions as option, i} {/if} @@ -313,14 +315,15 @@
Theme
- {#each themeOptions as option} + {#each themeOptions as option, i} {/each}
diff --git a/src/lib/components/TerminalTUI.svelte b/src/lib/components/TerminalTUI.svelte index 7dcbf4a..4047da4 100644 --- a/src/lib/components/TerminalTUI.svelte +++ b/src/lib/components/TerminalTUI.svelte @@ -61,6 +61,31 @@ let terminalElement: HTMLDivElement; let bodyElement = $state(); + // 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(); diff --git a/src/lib/components/tui/utils.ts b/src/lib/components/tui/utils.ts index 1793b1a..3a32851 100644 --- a/src/lib/components/tui/utils.ts +++ b/src/lib/components/tui/utils.ts @@ -21,7 +21,7 @@ export interface TextSegment { // Color maps for each theme export type ThemeColorMap = Record; -// 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', diff --git a/src/lib/keybinds.ts b/src/lib/keybinds.ts new file mode 100644 index 0000000..afd7b94 --- /dev/null +++ b/src/lib/keybinds.ts @@ -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}`); +} diff --git a/src/lib/pages/home.ts b/src/lib/pages/home.ts index 41e2a5d..cff889b 100644 --- a/src/lib/pages/home.ts +++ b/src/lib/pages/home.ts @@ -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: '' }, ]; \ No newline at end of file diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts index a6c7372..c02ca83 100644 --- a/src/lib/stores/theme.ts +++ b/src/lib/stores/theme.ts @@ -48,30 +48,27 @@ const themes: Record = { // Export themes for external access export { themes }; -// CSS variable mappings for theme colors - these update dynamically with mode changes -const themeColorVars: Record = { - '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, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e371f4a..0fe8d31 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,17 +1,19 @@