diff --git a/README.md b/README.md index 0b14be1..11b36a3 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ An Arch Linux terminal-themed portfolio website with Hyprland-style TUI componen - **Portfolio** (`/portfolio`) - Skills, projects, and contact info - **Models** (`/models`) - 3D model gallery with interactive viewer - **Hackathons** (`/hackathons`) - Hackathon projects and achievements +- **Components** (`/components`) - Showcase of all TUI components ## Configuration @@ -198,10 +199,24 @@ const lines: TerminalLine[] = [ { type: 'info', content: 'Information' }, // Primary with › prefix { type: 'header', content: 'Section Title' }, // Bold with # icon { type: 'blank', content: '' }, // Empty line - { type: 'divider', content: 'SECTION' }, // Horizontal divider + { type: 'divider', content: 'SECTION', id: 'section' }, // Horizontal divider with anchor ID ]; ``` +### Line Properties + +All line types support these optional properties: + +```typescript +{ + type: 'output', + content: 'Hello world', + id: 'my-section', // Anchor ID for URL hash scrolling (e.g., /page#my-section) + inline: true, // Render inline with adjacent inline elements + delay: 500, // Delay before this line appears (ms) +} +``` + ### Button (Full-width interactive) ```typescript @@ -212,11 +227,28 @@ const lines: TerminalLine[] = [ style: 'primary', // primary | accent | warning | error href: 'https://github.com', // URL to navigate to external: true, // Open in new tab (auto-detected for http/https) + inline: true, // Render as compact inline button // OR action: () => doSomething(), // Custom action } ``` +### Inline Elements + +Multiple elements can be rendered on the same line using `inline: true`: + +```typescript +// These will appear on the same line +{ type: 'output', content: 'Status:', inline: true }, +{ type: 'success', content: 'Online', inline: true }, +{ type: 'button', content: 'Refresh', icon: 'mdi:refresh', inline: true }, + +// Next line without inline breaks the group +{ type: 'blank', content: '' }, +``` + +Supported inline types: `button`, `link`, `tooltip`, `progress`, `output`, `info`, `success`, `error`, `warning` + ### Link (Inline clickable text) ```typescript @@ -322,10 +354,29 @@ Main terminal component: title="~/directory" interactive={true} speed="normal" + autoscroll={true} onComplete={() => console.log('Done!')} /> ``` +Props: +- `lines` - Array of TerminalLine objects +- `title` - Terminal window title +- `interactive` - Enable keyboard navigation +- `speed` - Typing speed preset or multiplier +- `autoscroll` - Auto-scroll as content types (default: true) +- `onComplete` - Callback when typing animation finishes + +### Anchor Scrolling + +Add `id` to any line to create an anchor that can be linked to: + +```typescript +{ type: 'divider', content: 'SKILLS', id: 'skills' }, +``` + +Then link to it with `/portfolio#skills` - the page will scroll to that section after typing completes. + ### Component Structure ``` @@ -346,9 +397,29 @@ src/lib/components/ └── TuiTooltip.svelte # Hover tooltip ``` -## 3D Models +## 3D Model Viewer -Place `.glb` files in `/static/models/` and update the models page to display them. +The `ModelViewer` component provides an interactive Three.js viewer for `.glb` models. + +### Features +- **Mouse Controls**: Drag to rotate, scroll to zoom +- **Arrow Key Controls**: Use arrow keys to orbit the camera (click viewer to focus first) +- **Auto-rotate**: Toggle automatic rotation +- **Wireframe Mode**: View model wireframe +- **Adjustable Lighting**: Increase/decrease scene brightness +- **Fullscreen Mode**: Expand to full viewport (press `Escape` to exit) +- **Ground Plane**: Optional shadow-receiving ground + +### Usage + +```svelte + +``` + +Place `.glb` files in `/static/models/` and they'll be accessible at `/models/filename.glb`. ## Tech Stack @@ -384,3 +455,95 @@ bun run preview | `Y` | Skip typing animation | | `T` | Toggle dark/light mode | +### 3D Model Viewer + +| Key | Action | +|-----|--------| +| `←` | Rotate camera left | +| `→` | Rotate camera right | +| `↑` | Rotate camera up | +| `↓` | Rotate camera down | +| `Escape` | Exit fullscreen | + +## Theme System + +Themes are defined as JSON files in `src/lib/assets/themes/`. Each theme contains colors for both dark and light modes. + +### Theme File Structure + +```json +{ + "name": "Theme Name", + "icon": "🎨", + "dark": { + "colors": { + "primary": "#89b4fa", + "secondary": "#313244", + "accent": "#a6e3a1", + "background": "#1e1e2e", + "backgroundLight": "#313244", + "text": "#cdd6f4", + "textMuted": "#a6adc8", + "border": "#45475a", + "terminal": "#1e1e2e", + "terminalPrompt": "#cba6f7", + "terminalUser": "#a6e3a1", + "terminalPath": "#89b4fa" + }, + "colorMap": { + "red": "#f38ba8", + "green": "#a6e3a1", + "blue": "#89b4fa", + "primary": "var(--terminal-primary)", + "accent": "var(--terminal-accent)", + "muted": "var(--terminal-muted)" + } + }, + "light": { + "colors": { /* light mode colors */ }, + "colorMap": { /* light mode color map */ } + } +} +``` + +### Adding a New Theme + +1. Create a new file: `src/lib/assets/themes/mytheme.theme.json` +2. Import it in `src/lib/stores/theme.ts`: + ```typescript + import myTheme from '$lib/assets/themes/mytheme.theme.json'; + ``` +3. Add it to the themes object: + ```typescript + const themes: Record = { + arch: archTheme, + catppuccin: catppuccinTheme, + mytheme: myTheme as ThemeJson + }; + ``` +4. Update the `ColorTheme` type to include your theme name + +### Available Themes +- **Arch Linux** (`arch`) - Classic terminal colors with Arch blue +- **Catppuccin** (`catppuccin`) - Soft, pastel Mocha/Latte colors + +### Theme-Specific Colors + +Beyond the basic colors, themes include: +- `teal`, `sky`, `sapphire`, `lavender` +- `peach`, `maroon`, `mauve` +- `flamingo`, `rosewater` + +```typescript +// These colors adapt to the current theme +'(&teal)Teal text(&)' +'(&lavender)Lavender text(&)' +'(&peach)Peach text(&)' +``` + +## Mobile Considerations + +- **Viewport height**: Uses `100dvh` (dynamic viewport height) to properly handle mobile browser chrome +- **Background color**: A fallback dark background (`#1e1e2e`) is set on `html` and `body` to prevent white bars when the page content doesn't fill the viewport +- **Overflow handling**: Hidden horizontal scrollbar to prevent accidental horizontal scroll on mobile + diff --git a/src/lib/assets/themes/arch.theme.json b/src/lib/assets/themes/arch.theme.json new file mode 100644 index 0000000..db1347f --- /dev/null +++ b/src/lib/assets/themes/arch.theme.json @@ -0,0 +1,96 @@ +{ + "name": "Arch Linux", + "icon": "🐧", + "dark": { + "colors": { + "primary": "#1793d1", + "secondary": "#333333", + "accent": "#23d18b", + "background": "#0d1117", + "backgroundLight": "#161b22", + "text": "#c9d1d9", + "textMuted": "#8b949e", + "border": "#30363d", + "terminal": "#0d1117", + "terminalPrompt": "#1793d1", + "terminalUser": "#23d18b", + "terminalPath": "#1793d1" + }, + "colorMap": { + "red": "#fa7970", + "green": "#23d18b", + "yellow": "#faa356", + "blue": "#1793d1", + "magenta": "#cea5fb", + "cyan": "#89dceb", + "white": "#c9d1d9", + "gray": "#6e7681", + "orange": "#ff9500", + "pink": "#ff79c6", + "black": "#0d1117", + "surface": "#161b22", + "teal": "#77dfd8", + "sky": "#00bfff", + "sapphire": "#0099cc", + "lavender": "#b4a7d6", + "peach": "#ff9966", + "maroon": "#fa7970", + "mauve": "#cea5fb", + "flamingo": "#e06c75", + "rosewater": "#e8b4b8", + "primary": "var(--terminal-primary)", + "accent": "var(--terminal-accent)", + "muted": "var(--terminal-muted)", + "error": "#fa7970", + "success": "#23d18b", + "warning": "#faa356", + "info": "#1793d1" + } + }, + "light": { + "colors": { + "primary": "#1793d1", + "secondary": "#e0e0e0", + "accent": "#005a2b", + "background": "#f6f8fa", + "backgroundLight": "#ffffff", + "text": "#111111", + "textMuted": "#57606a", + "border": "#d0d7de", + "terminal": "#ffffff", + "terminalPrompt": "#005a87", + "terminalUser": "#005a2b", + "terminalPath": "#005a87" + }, + "colorMap": { + "red": "#a6101d", + "green": "#005a2b", + "yellow": "#945b00", + "blue": "#0a5696", + "magenta": "#7820a0", + "cyan": "#006060", + "white": "#111111", + "gray": "#57606a", + "orange": "#c04500", + "pink": "#a01b58", + "black": "#ffffff", + "surface": "#eaeef2", + "teal": "#006060", + "sky": "#005a87", + "sapphire": "#004870", + "lavender": "#4a4ec0", + "peach": "#b05010", + "maroon": "#802020", + "mauve": "#602090", + "flamingo": "#901040", + "rosewater": "#a05040", + "primary": "var(--terminal-primary)", + "accent": "var(--terminal-accent)", + "muted": "var(--terminal-muted)", + "error": "#a6101d", + "success": "#005a2b", + "warning": "#945b00", + "info": "#0a5696" + } + } +} \ No newline at end of file diff --git a/src/lib/assets/themes/catppuccin.theme.json b/src/lib/assets/themes/catppuccin.theme.json new file mode 100644 index 0000000..e799121 --- /dev/null +++ b/src/lib/assets/themes/catppuccin.theme.json @@ -0,0 +1,96 @@ +{ + "name": "Catppuccin", + "icon": "🐱", + "dark": { + "colors": { + "primary": "#89b4fa", + "secondary": "#313244", + "accent": "#a6e3a1", + "background": "#1e1e2e", + "backgroundLight": "#313244", + "text": "#cdd6f4", + "textMuted": "#a6adc8", + "border": "#45475a", + "terminal": "#1e1e2e", + "terminalPrompt": "#cba6f7", + "terminalUser": "#a6e3a1", + "terminalPath": "#89b4fa" + }, + "colorMap": { + "red": "#f38ba8", + "green": "#a6e3a1", + "yellow": "#f9e2af", + "blue": "#89b4fa", + "magenta": "#cba6f7", + "cyan": "#94e2d5", + "white": "#cdd6f4", + "gray": "#6c7086", + "orange": "#fab387", + "pink": "#f5c2e7", + "black": "#1e1e2e", + "surface": "#313244", + "teal": "#94e2d5", + "sky": "#89dceb", + "sapphire": "#74c7ec", + "lavender": "#b4befe", + "peach": "#fab387", + "maroon": "#eba0ac", + "mauve": "#cba6f7", + "flamingo": "#f2cdcd", + "rosewater": "#f5e0dc", + "primary": "var(--terminal-primary)", + "accent": "var(--terminal-accent)", + "muted": "var(--terminal-muted)", + "error": "#f38ba8", + "success": "#a6e3a1", + "warning": "#f9e2af", + "info": "#89b4fa" + } + }, + "light": { + "colors": { + "primary": "#1e66f5", + "secondary": "#ccd0da", + "accent": "#40a02b", + "background": "#eff1f5", + "backgroundLight": "#dce0e8", + "text": "#4c4f69", + "textMuted": "#6c6f85", + "border": "#bcc0cc", + "terminal": "#eff1f5", + "terminalPrompt": "#8839ef", + "terminalUser": "#40a02b", + "terminalPath": "#1e66f5" + }, + "colorMap": { + "red": "#d20f39", + "green": "#40a02b", + "yellow": "#df8e1d", + "blue": "#1e66f5", + "magenta": "#8839ef", + "cyan": "#179299", + "white": "#4c4f69", + "gray": "#9ca0b0", + "orange": "#fe640b", + "pink": "#ea76cb", + "black": "#eff1f5", + "surface": "#ccd0da", + "teal": "#179299", + "sky": "#04a5e5", + "sapphire": "#209fb5", + "lavender": "#7287fd", + "peach": "#fe640b", + "maroon": "#e64553", + "mauve": "#8839ef", + "flamingo": "#dd7878", + "rosewater": "#dc8a78", + "primary": "var(--terminal-primary)", + "accent": "var(--terminal-accent)", + "muted": "var(--terminal-muted)", + "error": "#d20f39", + "success": "#40a02b", + "warning": "#df8e1d", + "info": "#1e66f5" + } + } +} diff --git a/src/lib/components/ModelViewer.svelte b/src/lib/components/ModelViewer.svelte index d74207c..63e767d 100644 --- a/src/lib/components/ModelViewer.svelte +++ b/src/lib/components/ModelViewer.svelte @@ -268,6 +268,67 @@ controls.update(); } + function rotateCamera(direction: 'left' | 'right' | 'up' | 'down') { + if (!controls) return; + + const rotateSpeed = 0.1; + const spherical = new THREE.Spherical(); + const offset = new THREE.Vector3(); + + // Get current camera position relative to target + offset.copy(camera.position).sub(controls.target); + spherical.setFromVector3(offset); + + switch (direction) { + case 'left': + spherical.theta += rotateSpeed; + break; + case 'right': + spherical.theta -= rotateSpeed; + break; + case 'up': + spherical.phi = Math.max(0.1, spherical.phi - rotateSpeed); + break; + case 'down': + spherical.phi = Math.min(Math.PI - 0.1, spherical.phi + rotateSpeed); + break; + } + + offset.setFromSpherical(spherical); + camera.position.copy(controls.target).add(offset); + controls.update(); + } + + function handleKeydown(event: KeyboardEvent) { + // Only handle if this component is focused or in fullscreen + if (!container?.contains(document.activeElement) && !isFullscreen) return; + + switch (event.key) { + case 'ArrowLeft': + event.preventDefault(); + rotateCamera('left'); + break; + case 'ArrowRight': + event.preventDefault(); + rotateCamera('right'); + break; + case 'ArrowUp': + event.preventDefault(); + rotateCamera('up'); + break; + case 'ArrowDown': + event.preventDefault(); + rotateCamera('down'); + break; + case 'Escape': + if (isFullscreen) { + isFullscreen = false; + setTimeout(handleResize, 100); + } + break; + } + } + function toggleControlsPanel() { showControls = !showControls; } @@ -277,6 +338,7 @@ initScene(); animate(); window.addEventListener('resize', handleResize); + window.addEventListener('keydown', handleKeydown); } }); @@ -288,6 +350,7 @@ renderer.dispose(); } window.removeEventListener('resize', handleResize); + window.removeEventListener('keydown', handleKeydown); }); @@ -410,7 +473,8 @@ {/if} -
+ +
{#if isLoading}
@@ -435,6 +499,10 @@ Scroll to zoom + + + Arrow keys +
@@ -557,6 +625,11 @@ color-mix(in srgb, var(--viewer-primary) 5%, transparent), transparent 70% ); + outline: none; + } + + .canvas-container:focus { + box-shadow: inset 0 0 0 2px var(--viewer-primary); } .canvas-container :global(canvas) { diff --git a/src/lib/components/NavbarWaybar.svelte b/src/lib/components/NavbarWaybar.svelte index 35ce485..bd39cb1 100644 --- a/src/lib/components/NavbarWaybar.svelte +++ b/src/lib/components/NavbarWaybar.svelte @@ -142,7 +142,7 @@ {:else if currentPath === '/models'} - {:else if currentPath === '/hackathons'} + {:else if currentPath === '/projects'} {:else} diff --git a/src/lib/components/TerminalTUI.svelte b/src/lib/components/TerminalTUI.svelte index 6521924..9350e61 100644 --- a/src/lib/components/TerminalTUI.svelte +++ b/src/lib/components/TerminalTUI.svelte @@ -1,5 +1,6 @@
- {#each displayedLines as { parsed, charIndex, complete, showImage }, i} - {@const line = parsed.line} - {@const visibleSegments = getSegmentsUpToChar(parsed.segments, charIndex)} - {#if line.type === 'divider'} -
- - {#if line.content} - {line.content} - {/if} - + {#each groupedLines as group} + {#if group.type === 'inline-group'} + +
+ {#each group.items as displayed, j} + {@const line = displayed.parsed.line} + {@const idx = group.indices[j]} + {@const visibleSegments = getSegmentsUpToChar(displayed.parsed.segments, displayed.charIndex)} + {#if line.type === 'button'} + + {:else if line.type === 'link'} + onLinkClick(idx)} /> + {:else if line.type === 'tooltip'} + + {:else if line.type === 'progress'} + + {:else} + + {getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}{:else if getSegmentStyle(segment)}{segment.text}{:else}{segment.text}{/if}{/each} + + {/if} + {/each}
- {:else if line.type === 'button'} - - {:else if line.type === 'link'} - - {:else if line.type === 'card'} - - {:else if line.type === 'cardgrid'} - - {:else if line.type === 'progress'} - - {:else if line.type === 'accordion'} - - {:else if line.type === 'table'} - - {:else if line.type === 'tooltip'} -
- -
- {:else} -
- {#if line.type === 'command' || line.type === 'prompt'} - - {user.username}@{user.hostname} - :~$ - - {/if} + {:else if group.type === 'single'} + {@const i = group.index} + {@const displayed = group.displayed} + {@const line = displayed.parsed.line} + {@const visibleSegments = getSegmentsUpToChar(displayed.parsed.segments, displayed.charIndex)} + {@const complete = displayed.complete} + {@const showImage = displayed.showImage} + {#if line.type === 'divider'} +
+ + {#if line.content} + {line.content} + {/if} + +
+ {:else if line.type === 'button'} + + {:else if line.type === 'link'} + + {:else if line.type === 'card'} + + {:else if line.type === 'cardgrid'} + + {:else if line.type === 'progress'} + + {:else if line.type === 'accordion'} + + {:else if line.type === 'table'} + + {:else if line.type === 'tooltip'} +
+ +
+ {:else} +
+ {#if line.type === 'command' || line.type === 'prompt'} + + {user.username}@{user.hostname} + :~$ + + {/if} - {#if line.type === 'image' && showImage} -
- {line.imageAlt - {#if line.content} - {line.content} - {/if} -
- {:else if line.type === 'header'} - - - {#each visibleSegments as segment} - {#if segment.icon} - - {:else if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} + {#if line.type === 'image' && showImage} +
+ {line.imageAlt + {#if line.content} + {line.content} {/if} - {/each} - - {:else if line.type !== 'blank'} - - {getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}{:else if getSegmentStyle(segment)}{segment.text}{:else}{segment.text}{/if}{/each} - - {/if} +
+ {:else if line.type === 'header'} + + + {#each visibleSegments as segment} + {#if segment.icon} + + {:else if getSegmentStyle(segment)} + {segment.text} + {:else} + {segment.text} + {/if} + {/each} + + {:else if line.type !== 'blank'} + + {getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}{:else if getSegmentStyle(segment)}{segment.text}{:else}{segment.text}{/if}{/each} + + {/if} - {#if terminalSettings.showCursor && i === currentLineIndex && !complete && isTyping && line.type !== 'image' && line.type !== 'blank'} - - {/if} -
+ {#if terminalSettings.showCursor && i === currentLineIndex && !complete && isTyping && line.type !== 'image' && line.type !== 'blank'} + + {/if} +
+ {/if} {/if} {/each} @@ -120,6 +178,40 @@ min-height: 0; } + /* Inline group wrapper - groups consecutive inline elements */ + .tui-inline-group { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.2rem; + animation: lineSlideIn 0.15s ease-out; + } + + .inline-content { + display: inline; + white-space: pre-wrap; + } + + .inline-content.output { + color: var(--terminal-muted); + } + + .inline-content.info { + color: var(--terminal-primary); + } + + .inline-content.success { + color: #a6e3a1; + } + + .inline-content.error { + color: #f38ba8; + } + + .inline-content.warning { + color: #f9e2af; + } + /* Lines */ .tui-line { display: flex; diff --git a/src/lib/components/tui/TuiButton.svelte b/src/lib/components/tui/TuiButton.svelte index a1fdc52..f9180bc 100644 --- a/src/lib/components/tui/TuiButton.svelte +++ b/src/lib/components/tui/TuiButton.svelte @@ -1,6 +1,6 @@ {#if isExternal} @@ -62,4 +73,10 @@ .tui-link:hover :global(.link-external) { opacity: 0.8; } + + :global(.inline-icon) { + display: inline-block; + vertical-align: middle; + margin: 0 0.15em; + } diff --git a/src/lib/components/tui/TuiProgress.svelte b/src/lib/components/tui/TuiProgress.svelte index 929200f..c66854c 100644 --- a/src/lib/components/tui/TuiProgress.svelte +++ b/src/lib/components/tui/TuiProgress.svelte @@ -1,16 +1,30 @@ -
+
{#if line.content} -
{line.content}
+
+ {#each contentSegments as segment} + {#if segment.icon} + + {:else if getSegmentStyle(segment)} + {segment.text} + {:else} + {segment.text} + {/if} + {/each} +
{/if}
@@ -22,7 +36,17 @@ {/each}
-
{label}
+
+ {#each labelSegments as segment} + {#if segment.icon} + + {:else if getSegmentStyle(segment)} + {segment.text} + {:else} + {segment.text} + {/if} + {/each} +
diff --git a/src/lib/components/tui/types.ts b/src/lib/components/tui/types.ts index a1fd8fb..cfb704e 100644 --- a/src/lib/components/tui/types.ts +++ b/src/lib/components/tui/types.ts @@ -13,6 +13,10 @@ export interface TerminalLine { image?: string; imageAlt?: string; imageWidth?: number; + // For anchor scrolling (e.g., #skills) + id?: string; + // For inline rendering (multiple elements on same line) + inline?: boolean; // For button and link types action?: () => void; href?: string; diff --git a/src/lib/components/tui/utils.ts b/src/lib/components/tui/utils.ts index c1f4962..bbfdcba 100644 --- a/src/lib/components/tui/utils.ts +++ b/src/lib/components/tui/utils.ts @@ -18,9 +18,11 @@ export interface TextSegment { action?: () => void; } -// Color map for text and background colors -export const colorMap: Record = { - // Basic colors +// Color maps for each theme +export type ThemeColorMap = Record; + +// Default color map (fallback) +export const defaultColorMap: ThemeColorMap = { 'red': '#f38ba8', 'green': '#a6e3a1', 'yellow': '#f9e2af', @@ -33,7 +35,6 @@ export const colorMap: Record = { 'pink': '#f5c2e7', 'black': '#1e1e2e', 'surface': '#313244', - // Semantic colors 'primary': 'var(--terminal-primary)', 'accent': 'var(--terminal-accent)', 'muted': 'var(--terminal-muted)', @@ -43,10 +44,13 @@ export const colorMap: Record = { 'info': '#89b4fa', }; +// Legacy alias for backwards compatibility +export const colorMap = defaultColorMap; + // Text style keywords const textStyles = ['bold', 'dim', 'italic', 'underline', 'strikethrough', 'overline']; -export function parseColorText(text: string): TextSegment[] { +export function parseColorText(text: string, colors: ThemeColorMap = colorMap): TextSegment[] { const segments: TextSegment[] = []; // Match both (&specs)content(&) and (&icon, iconName) patterns const regex = /\(&([^)]+)\)(.*?)\(&\)|\(&icon,\s*([^)]+)\)/g; @@ -81,15 +85,15 @@ export function parseColorText(text: string): TextSegment[] { // Background color (bg-colorname or bg-#hex) else if (spec.startsWith('bg-')) { const bgColor = spec.slice(3); - if (colorMap[bgColor]) { - segment.background = colorMap[bgColor]; + if (colors[bgColor]) { + segment.background = colors[bgColor]; } else if (bgColor.startsWith('#')) { segment.background = bgColor; } } // Foreground color - else if (colorMap[spec] && !textStyles.includes(spec)) { - segment.color = colorMap[spec]; + else if (colors[spec] && !textStyles.includes(spec)) { + segment.color = colors[spec]; } else if (spec.startsWith('#')) { segment.color = spec; } diff --git a/src/lib/config.ts b/src/lib/config.ts index 9e7ccbd..117d204 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -11,11 +11,11 @@ export const user = { title: 'Engineering Student', email: 'sirblob0@gmail.com', location: 'Washington DC-Baltimore Area', - bio: `Hi, I am Sir Blob — a engineer who loves making things.` + + bio: `Hi, I am Sir Blob — a engineer who loves making things. ` + `I build fun coding projects, participate in game jams and hackathons, and enjoy games like Minecraft and Pokémon TCG Live.`, // Prefer an absolute avatar URL if you want to pull directly from GitHub - avatar: 'https://avatars.githubusercontent.com/u/76974209?v=4', + avatar: '/blob_nerd.png', // Social links - array of { name, icon (Iconify), link } socials: [ @@ -113,12 +113,11 @@ export const terminalButtons = { export const skills = { languages: ['Python', 'JavaScript', 'TypeScript', 'C', 'C++', 'Java', 'Node.js'], - frameworks: ['Arduino', 'Bootstrap', 'TailwindCSS', 'Discord.js', 'React', 'Electron', 'Svelte', 'MongoDB'], - // Applications / IDEs and major platforms + frameworks: ['Arduino', 'Bootstrap', 'TailwindCSS', 'Discord.js', 'React', 'Electron', 'Svelte'], applications: ['Windows', 'Linux', 'macOS', 'IntelliJ', 'VS Code', 'Git', 'Blender', 'Godot'], - tools: ['Git', 'Docker', 'Neovim', 'VS Code', 'Figma'], - databases: ['PostgreSQL', 'MongoDB', 'Redis', 'SQLite'], - cloud: ['AWS', 'Vercel', 'Cloudflare', 'DigitalOcean'], + platforms: ['Windows', 'Linux', 'macOS', 'Arduino', 'Raspberry Pi'], + tools: ['Git', 'Docker', 'Neovim', 'VS Code'], + databases: ['MongoDB', 'Redis', 'SQLite'], interests: ['Open Source', '3D Graphics', 'CLI Tools', 'Game Dev'] }; @@ -485,7 +484,7 @@ export const pageSpeedSettings: Record = { 'home': 'fast', 'portfolio': 'fast', 'models': 'fast', - 'hackathons': 'normal' + 'projects': 'normal' }; // Per-page autoscroll settings (whether to auto-scroll as content types) @@ -496,18 +495,18 @@ export const pageAutoscrollSettings: Record = { // 'portfolio': false, // Disable autoscroll 'home': true, 'portfolio': false, - 'models': true, - 'hackathons': false, - 'components': true + 'models': false, + 'projects': false, + 'components': false }; // Speed preset multipliers (lower = faster) export const speedPresets: Record = { 'instant': 0, // No delay, instant display - 'fast': 0.3, // 3x faster - 'normal': 0.8, // Default speed - 'slow': 2, // 2x slower - 'typewriter': 3 // 3x slower, classic typewriter feel + 'fast': 0.1, // 3x faster + 'normal': 0.5, // Default speed + 'slow': 1.5, // 2x slower + 'typewriter': 2 // 3x slower, classic typewriter feel }; // ============================================================================ @@ -685,7 +684,7 @@ export const loadingScreen = { // ============================================================================ export const site = { - title: `${user.username} | Portfolio`, + title: `${user.displayname} | Portfolio`, description: `${user.title} - ${user.bio}`, keywords: ['developer', 'portfolio', 'programming', ...skills.languages, ...skills.frameworks], ogImage: '/og-image.png', @@ -706,31 +705,31 @@ export interface PageMeta { // Map route => meta. Use route paths as keys (e.g. '/', '/portfolio') export const pageMeta: Record = { '/': { - title: `${user.username} — Home`, + title: `${user.displayname} — Home`, description: `Home — ${user.title} portfolio and projects.`, icon: 'mdi:home', keywords: ['home', 'portfolio', 'about'] }, '/portfolio': { - title: `${user.username} — Portfolio`, + title: `${user.displayname} — Portfolio`, description: 'Selected projects, highlights and case studies.', icon: 'mdi:folder-multiple', keywords: ['projects', 'portfolio', 'showcase'] }, '/models': { - title: `${user.username} — 3D Models`, + title: `${user.displayname} — 3D Models`, description: 'A curated collection of 3D models and previews.', icon: 'mdi:cube-outline', keywords: ['3d', 'models', 'glb', 'gltf'] }, - '/hackathons': { - title: `${user.username} — Hackathons`, + '/projects': { + title: `${user.displayname} — Projects`, description: 'Hackathon projects, demos and awards.', icon: 'mdi:trophy', keywords: ['hackathon', 'projects', 'events'] }, '/components': { - title: `${user.username} — Components`, + title: `${user.displayname} — Components`, description: 'Terminal UI components showcase and documentation.', icon: 'mdi:puzzle', keywords: ['components', 'ui', 'terminal', 'tui'] @@ -757,7 +756,7 @@ export const navigation = [ { name: 'home', path: '/', icon: '~' }, { name: 'portfolio', path: '/portfolio', icon: '📁' }, { name: 'models', path: '/models', icon: '🎨' }, - { name: 'hackathons', path: '/hackathons', icon: '🏆' }, + { name: 'projects', path: '/projects', icon: '🏆' }, // { name: 'components', path: '/components', icon: '🧩' }, { name: 'blog', path: 'https://blog.sirblob.co', icon: '📝', external: true } ]; diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts index 7dae86a..9b387d5 100644 --- a/src/lib/stores/theme.ts +++ b/src/lib/stores/theme.ts @@ -1,9 +1,28 @@ import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; +import { type ThemeColorMap, defaultColorMap } from '$lib/components/tui/utils'; + +// Import theme JSON files +import archTheme from '$lib/assets/themes/arch.theme.json'; +import catppuccinTheme from '$lib/assets/themes/catppuccin.theme.json'; export type ColorTheme = 'arch' | 'catppuccin'; export type Mode = 'dark' | 'light'; +// Theme JSON structure +export interface ThemeJson { + name: string; + icon: string; + dark: { + colors: Record; + colorMap: ThemeColorMap; + }; + light: { + colors: Record; + colorMap: ThemeColorMap; + }; +} + export interface ThemeColors { primary: string; secondary: string; @@ -17,73 +36,50 @@ export interface ThemeColors { terminalPrompt: string; terminalUser: string; terminalPath: string; + colorMap: ThemeColorMap; } -const themeColorsMap: Record = { - arch: { - dark: { - primary: '#1793d1', - secondary: '#333333', - accent: '#00ff00', - background: '#0d1117', - backgroundLight: '#161b22', - text: '#c9d1d9', - textMuted: '#8b949e', - border: '#30363d', - terminal: '#0d1117', - terminalPrompt: '#1793d1', - terminalUser: '#00ff00', - terminalPath: '#1793d1' - }, - light: { - primary: '#1793d1', - secondary: '#e0e0e0', - accent: '#006600', - background: '#f6f8fa', - backgroundLight: '#ffffff', - text: '#1a1a1a', - textMuted: '#4a5568', - border: '#d0d7de', - terminal: '#ffffff', - terminalPrompt: '#0f6ca0', - terminalUser: '#006600', - terminalPath: '#0f6ca0' - } - }, - catppuccin: { - // Catppuccin Mocha (dark) - dark: { - primary: '#89b4fa', // Blue - secondary: '#313244', // Surface 0 - accent: '#a6e3a1', // Green - background: '#1e1e2e', // Base - backgroundLight: '#313244', // Surface 0 - text: '#cdd6f4', // Text - textMuted: '#a6adc8', // Subtext 0 - border: '#45475a', // Surface 1 - terminal: '#1e1e2e', // Base - terminalPrompt: '#cba6f7', // Mauve - terminalUser: '#a6e3a1', // Green - terminalPath: '#89b4fa' // Blue - }, - // Catppuccin Latte (light) - light: { - primary: '#1e66f5', // Blue - secondary: '#ccd0da', // Surface 0 - accent: '#40a02b', // Green - background: '#eff1f5', // Base - backgroundLight: '#dce0e8', // Crust - text: '#4c4f69', // Text - textMuted: '#6c6f85', // Subtext 0 - border: '#bcc0cc', // Surface 1 - terminal: '#eff1f5', // Base - terminalPrompt: '#8839ef', // Mauve - terminalUser: '#40a02b', // Green - terminalPath: '#1e66f5' // Blue - } - } +// Load themes from JSON files +const themes: Record = { + arch: archTheme as ThemeJson, + catppuccin: catppuccinTheme as ThemeJson }; +// Export themes for external access +export { themes }; + +// Build theme colors from JSON +function buildThemeColors(theme: ThemeJson, mode: Mode): ThemeColors { + const modeData = theme[mode]; + return { + primary: modeData.colors.primary, + secondary: modeData.colors.secondary, + accent: modeData.colors.accent, + background: modeData.colors.background, + backgroundLight: modeData.colors.backgroundLight, + text: modeData.colors.text, + textMuted: modeData.colors.textMuted, + border: modeData.colors.border, + terminal: modeData.colors.terminal, + terminalPrompt: modeData.colors.terminalPrompt, + terminalUser: modeData.colors.terminalUser, + terminalPath: modeData.colors.terminalPath, + colorMap: modeData.colorMap ?? defaultColorMap + }; +} + +// Build the theme colors map from JSON themes +const themeColorsMap: Record = + Object.fromEntries( + Object.entries(themes).map(([key, theme]) => [ + key, + { + dark: buildThemeColors(theme, 'dark'), + light: buildThemeColors(theme, 'light') + } + ]) + ) as Record; + function getInitialMode(): Mode { if (browser) { const stored = localStorage.getItem('mode'); @@ -129,7 +125,10 @@ export function setColorTheme(theme: ColorTheme) { colorTheme.set(theme); } -export const themeOptions: { value: ColorTheme; label: string; icon: string }[] = [ - { value: 'arch', label: 'Arch Linux', icon: '🐧' }, - { value: 'catppuccin', label: 'Catppuccin', icon: '🐱' } -]; +// Generate theme options from loaded themes +export const themeOptions: { value: ColorTheme; label: string; icon: string }[] = + Object.entries(themes).map(([key, theme]) => ({ + value: key as ColorTheme, + label: theme.name, + icon: theme.icon + })); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 04dd033..906d98d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -12,17 +12,18 @@ const lines: TerminalLine[] = [ // neofetch style intro { type: 'command', content: 'bash ~/startup.sh', delay: 300 }, + + // Color palette (moved below the art/stats block) + { type: 'blank', content: '' }, + { type: 'output', content: '(&red)███(&)(&orange)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)(&pink)███(&)' }, + { type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)' }, + { type: 'blank', content: '' }, { type: 'header', content: `Welcome to ${user.displayname}'s Portfolio` }, { type: 'blank', content: '' }, { type: 'output', content: `(&muted)${user.bio}(&)` }, { type: 'blank', content: '' }, - // Color palette (moved below the art/stats block) - { type: 'blank', content: '' }, - { type: 'output', content: '(&red)███(&)(&orange)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)(&pink)███(&)' }, - { type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)' }, - { type: 'divider', content: 'NAVIGATION' }, { type: 'blank', content: '' }, diff --git a/src/routes/layout.css b/src/routes/layout.css index 2a38a4d..d462669 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -48,6 +48,7 @@ code, pre, kbd, samp, .terminal, .mono, .prompt, .prompt-mini, .hero-title { html { scroll-behavior: smooth; + background: #1e1e2e; /* Fallback dark bg to prevent white flash on mobile */ } body { @@ -58,6 +59,8 @@ body { -moz-osx-font-smoothing: grayscale; overflow-x: hidden; min-height: 100vh; + min-height: 100dvh; /* Dynamic viewport height for mobile */ + background: #1e1e2e; /* Fallback dark bg to prevent white flash on mobile */ } /* Terminal font for code elements */ diff --git a/src/routes/portfolio/+page.svelte b/src/routes/portfolio/+page.svelte index 1531555..cbb71e4 100644 --- a/src/routes/portfolio/+page.svelte +++ b/src/routes/portfolio/+page.svelte @@ -22,17 +22,23 @@ { type: 'info', content: `(&accent)${user.title}(&)` }, { type: 'output', content: `(&muted)${user.bio}(&)` }, { type: 'blank', content: '' }, + { type: 'output', content: `(&primary, bold)Links >(&)`, inline: true }, + { type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true }, + { type: 'link', href: "/portfolio#skills", content: `(&bg-orange,black)Skills(&)`, inline: true }, + { type: 'link', href: "/portfolio#projects", content: `(&bg-green,black)Projects(&)`, inline: true }, + { type: 'blank', content: '' }, - { type: 'divider', content: 'CONTACT' }, + { type: 'divider', content: 'CONTACT', id: 'contact' }, { type: 'blank', content: '' }, // Contact buttons - dynamically generated from socials array ...user.socials.map(social => ({ type: 'button' as const, - content: `${social.name}: ${social.link}`, + content: `${social.name}`, icon: social.icon, style: 'primary' as const, - href: social.link + href: social.link, + inline: true })), // { // type: 'button', @@ -43,7 +49,7 @@ // }, { type: 'blank', content: '' }, - { type: 'divider', content: 'SKILLS' }, + { type: 'divider', content: 'SKILLS', id: 'skills' }, { type: 'blank', content: '' }, // Skills as TUI sections @@ -59,14 +65,11 @@ { type: 'info', content: '(&cyan,bold)▸ Databases(&)' }, { type: 'output', content: ' ' + skills.databases.map(s => `(&cyan)${s}(&)`).join(' (&muted)•(&) ') }, { type: 'blank', content: '' }, - { type: 'info', content: '(&yellow,bold)▸ Cloud(&)' }, - { type: 'output', content: ' ' + skills.cloud.map(s => `(&orange)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, { type: 'info', content: '(&accent,bold)▸ Interests(&)' }, { type: 'output', content: ' ' + skills.interests.map(s => `(&muted)${s}(&)`).join(' (&muted)•(&) ') }, { type: 'blank', content: '' }, - { type: 'divider', content: 'PROJECTS' }, + { type: 'divider', content: 'PROJECTS', id: 'projects' }, { type: 'blank', content: '' }, // Featured projects with buttons @@ -119,7 +122,7 @@ - Portfolio | {user.name} + Portfolio | {user.displayname} diff --git a/src/routes/hackathons/+page.svelte b/src/routes/projects/+page.svelte similarity index 74% rename from src/routes/hackathons/+page.svelte rename to src/routes/projects/+page.svelte index 753d1af..9c8bfae 100644 --- a/src/routes/hackathons/+page.svelte +++ b/src/routes/projects/+page.svelte @@ -4,8 +4,8 @@ import { user, sortedCards } from '$lib/config'; import { getPageSpeedMultiplier, getPageAutoscroll } from '$lib'; - const speed = getPageSpeedMultiplier('hackathons'); - const autoscroll = getPageAutoscroll('hackathons'); + const speed = getPageSpeedMultiplier('projects'); + const autoscroll = getPageAutoscroll('projects'); // Count stats const totalHackathons = sortedCards.length; @@ -14,12 +14,15 @@ // Build the terminal lines with card grid const lines: TerminalLine[] = [ + { type: 'divider', content: 'PROJECTS' }, + { type: 'command', content: 'ls ~/projects --grid' }, + { type: 'blank', content: '' }, + { type: 'divider', content: 'HACKATHONS' }, { type: 'command', content: 'ls ~/hackathons --grid' }, { type: 'blank', content: '' }, { type: 'header', content: `Hackathon Journey` }, { type: 'output', content: `(&muted)Total:(&) (&primary)${totalHackathons}(&) (&muted)| Awards:(&) (&yellow)${totalAwards}(&) (&muted)| Featured:(&) (&accent)${featuredCount}(&)` }, { type: 'blank', content: '' }, - // { type: 'divider', content: 'PROJECTS' }, { type: 'blank', content: '' }, { type: 'cardgrid', content: '', cards: sortedCards }, { type: 'blank', content: '' }, @@ -28,16 +31,16 @@ - Hackathons | {user.name} + Projects | {user.name} -
- +
+