diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1e71e2d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DISCORD_TOKEN= +DISCORD_USER_ID= +DISCORD_GUILD_ID= diff --git a/.gitignore b/.gitignore index 8d505b8..e466824 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ vite.config.ts.timestamp-* bun.lock Update.md -blogs/ \ No newline at end of file +PROJECT.md +content/blog/ +blogs/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5449017 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc31e15 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/README.md b/README.md index 2a8de8d..09117af 100644 --- a/README.md +++ b/README.md @@ -1,778 +1,149 @@ -# Terminal Portfolio +# Sir Blob's Desktop -An Arch Linux terminal-themed portfolio website with Hyprland-style TUI components, built with SvelteKit, Tailwind CSS, and Three.js. +A portfolio website that recreates my Arch Linux desktop (Hyprland, Quickshell bar, kitty terminals) in the browser. Built with SvelteKit 3, Svelte 5 runes and Tailwind CSS 4. -## Features +## Running -- Hyprland-style TUI - Terminal interface inspired by Textual Python TUI -- Interactive command line - Type commands directly into the terminal to navigate and run actions -- Theme Support - Arch, Catppuccin, Wintry, Rose, and Cerberus themes, each with dark/light variants -- Dark/Light Mode - Toggle between dark and light modes -- Keyboard Navigation - Navigate with arrow keys or vim-style j/k -- Native Blog - Markdown posts with tags, authors, syntax highlighting, search, pagination, and an RSS feed -- 3D Model Viewer - Interactive Three.js viewer for .glb models -- QR Links Page - Generate a scannable QR code for any of your links -- Configurable Speed - Per-page typing animation speed -- Responsive - Works on desktop and mobile -- Rich Text Formatting - Colors, backgrounds, text decorations, and sizeable inline icons - -## Pages - -- Home (`/`) - Neofetch-style intro with navigation -- About (`/about`) - Background and details -- Portfolio (`/portfolio`) - Profile, experience, skills, and contact info -- Projects (`/projects`) - Hub linking out to the three project categories below - - Open Source (`/projects/opensource`) - Projects built and maintained in the open - - Packages (`/projects/packages`) - Published packages and CLI tools - - Hackathons (`/projects/hackathons`) - Hackathon projects, awards, and stats -- Blog (`/blog`) - Native markdown blog with tags, search, pagination, and an RSS feed at `/blog/rss.xml` -- Models (`/models`) - 3D model gallery with interactive viewer -- Components (`/components`) - Showcase of all TUI components -- Links (`/links`) - All links with on-demand QR codes - -The navbar exposes Home, About, Projects, and Blog. Portfolio, Models, Components, and Links are reachable directly by URL or with the terminal `cd` command (for example, `cd links`). - -## Terminal Commands - -Every terminal page has an interactive command line at the bottom. Click the terminal (or just start typing once focused), enter a command, and press Enter. Output is printed back into the terminal. - -Use the Up/Down arrows to cycle through command history. - -| Command | Aliases | Description | -|---------|---------|-------------| -| `help` | `h`, `?` | List all available commands | -| `clear` | `cls` | Clear the terminal | -| `whoami` | | Show a short bio | -| `neofetch` | `banner` | Display system info with profile picture | -| `skills` | | List skills by category | -| `contact` | `socials` | Show social links as buttons | -| `email` | | Show contact email | -| `cd ` | `goto`, `open` | Navigate to a page, including subpages (e.g. `cd projects/opensource`) | -| `ls` | `dir` | List the pages you can visit as a tree, with project subpages nested under `projects/` | -| `theme [name]` | | List or set the color theme | -| `mode [dark\|light]` | | Toggle or set dark/light mode | -| `echo ` | | Print text back | -| `cowsay ` | | Make a cow say something | -| `date` | | Show the current date and time (Eastern) | - -Command definitions live in `src/lib/components/tui/terminal-commands.ts`. Add a new entry to the `commands` list to extend the command set. Pages and their subpages are declared once in the `pageTree` structure in that file, which both `ls` (tree rendering) and `cd` (nested path resolution) read from. - -## Configuration - -The site configuration is modular - split into focused files in `src/lib/config/` for easier maintenance. You can still import everything from `$lib/config` for backward compatibility. - -### Config File Structure - -``` -src/lib/config/ -├── index.ts # Barrel export (re-exports all modules) -├── user.ts # User profile, socials, links, skills -├── layout.ts # Layout dimensions, breakpoints, fonts, navbar, scrollbar -├── theme.ts # Colors, animations, effects, loading screen -├── content.ts # Projects, 3D models, experience, hackathon cards -├── terminal.ts # Terminal settings, TUI styling, speed presets, shortcuts -├── navigation.ts # Navigation links, site metadata, page meta -└── blog.ts # Blog name and tagline shown on /blog +```bash +bun install +bun run dev +bun run build +bun run check ``` -### Import Examples +## How the desktop works -```typescript -// Barrel import (backward compatible) -import { user, colorPalette, projects } from '$lib/config'; +- The top bar mirrors the blob-shell Quickshell bar: chai menu (app launcher), workspaces 1-9, clock, Discord status and theme picker. +- Every page (`/about`, `/projects/hackathons`, ...) opens as a tiled window, so old resume links still work. +- Terminals open next to it and tile with the Hyprland dwindle layout. +- Desktop icons open a page, a terminal running a command, or an external link. +- On small screens the focused window goes fullscreen and open windows are listed as tabs. -// Direct imports (smaller bundles, faster builds) -import { user, skills, links } from '$lib/config/user'; -import { colorPalette, animations } from '$lib/config/theme'; -import { openSourceProjects, cards, experience } from '$lib/config/content'; -import { terminalSettings, keyboardShortcuts } from '$lib/config/terminal'; -import { navigation, site, pageMeta } from '$lib/config/navigation'; -``` +### Keybinds -### Config Modules +| Keys | Action | +| --- | --- | +| Alt + Enter | Open a kitty terminal | +| Alt + Space | App launcher | +| Alt + Q | Close focused window | +| Alt + F | Fullscreen the page window | +| Alt + J / K | Focus next / previous window | +| Alt + 1-9 | Switch workspace | +| Alt + T | Next theme | + +Alt stands in for Super, which browsers do not receive. + +## Editing content + +Everything you would normally change lives in `content/`. No code changes needed. | File | Contents | -|------|----------| -| `user.ts` | `user`, `skills`, `links` | -| `layout.ts` | `layout`, `breakpoints`, `fonts`, `navbar`, `scrollbar` | -| `theme.ts` | `colorPalette`, `terminalButtons`, `loadingScreen`, `effects`, `animations` | -| `content.ts` | `openSourceProjects`, `packageProjects`, `models`, `experience`, `cards`, `sortedCards` + types | -| `terminal.ts` | `terminalSettings`, `tuiStyle`, `tuiText`, `pageSpeedSettings`, `pageAutoscrollSettings`, `speedPresets`, `modelViewer`, `particles`, `keyboardShortcuts` | -| `navigation.ts` | `navigation`, `site`, `pageMeta` | -| `blog.ts` | `blogName`, `blogTagline` | +| --- | --- | +| `content/profile.json` | name (shown only on the portfolio page), bio, avatar, education, socials (also used by the links page), skills, interests | +| `content/experience.json` | experience with highlights, tech, awards and links | +| `content/leadership.json` | leadership roles | +| `content/about.json` | hobbies, games, shows, movies | +| `content/projects/opensource.json` | open source projects | +| `content/projects/packages.json` | published packages | +| `content/projects/hackathons.json` | hackathon projects, events and awards | +| `content/projects/research.json` | research roles and projects | +| `content/models.json` | 3D models (files go in `static/models/`) | +| `content/desktop.json` | default theme, workspaces, desktop icons, startup command | +| `content/blog.json` | blog name, tagline, posts per page | +| `content/blog/*.md` | blog posts | -### Example: Key config snippets +### Projects -```typescript -// Toggle theme keys and other shortcuts -export const keyboardShortcuts = { - skip: ['y', 'Y'], // Skip typing animation - toggleTheme: ['t', 'T'], // Toggle dark/light mode - navigateUp: ['ArrowUp', 'k'], - navigateDown: ['ArrowDown', 'j'], - select: ['Enter'], -}; +Every project in every category uses the same shape. Only `name`, `description`, `tags` and `links` are required. -// Terminal / typing -export const terminalSettings = { - baseTypeSpeed: 20, - minTypeSpeed: 5, - maxTypeSpeed: 50, - startDelay: 300, - lineDelay: 100, - showCursor: true, - promptStyle: 'full', - scrollMargin: 80, -}; - -// Color palette (Catppuccin Mocha by default) -export const colorPalette = { - red: '#f38ba8', - green: '#a6e3a1', - yellow: '#f9e2af', - blue: '#89b4fa', - magenta: '#cba6f7', - cyan: '#94e2d5', - white: '#cdd6f4', - gray: '#6c7086', - error: '#f38ba8', - success: '#a6e3a1', -}; +```json +{ + "name": "HushMap", + "description": "What it does.", + "image": "/hacks/hushmap.png", + "tags": ["Svelte", "FastAPI"], + "featured": true, + "event": { "name": "Bitcamp 2026", "university": "University of Maryland", "location": "College Park, MD", "year": "2026" }, + "awards": [{ "track": "Best UI/UX Hack", "place": "Winner" }], + "demoMayBeOffline": false, + "version": "2.2.0", + "links": { "demo": "https://...", "code": "https://...", "devpost": "https://...", "package": "https://...", "article": "https://..." } +} ``` -### How to Customize +Featured projects are listed first. -- Edit `config/user.ts` to update your profile, socials, links, and skills -- Edit `config/content.ts` to add projects, models, experience, or hackathon entries -- Edit `config/theme.ts` to change colors, animations, or loading screen -- Edit `config/terminal.ts` to adjust typing speed, TUI styling, or shortcuts -- Edit `config/layout.ts` to change dimensions, breakpoints, or navbar settings -- Edit `config/navigation.ts` to add/remove nav links or update page metadata +### Desktop icons -Changes take effect on next reload. Some values (fonts, CSS variables) may also require adjusting CSS or Tailwind config. +Each icon in `content/desktop.json` has a `label`, an Iconify `icon` and one `open` action: -### Where to Look for Types & Utilities - -- `src/lib/config/` - All configuration modules -- `src/lib/components/tui/types.ts` - TerminalLine types -- `src/lib/components/tui/utils.ts` - Parsing utilities and style helpers -- `src/lib/components/tui/terminal-commands.ts` - Interactive command definitions -- `src/lib/stores/theme.ts` - Theme store & `toggleMode()` -- `src/lib/index.ts` - Helper functions (barrel export) - -## Links Page - -The `/links` page lists everything in the `links` array (a dedicated list, separate from `user.socials`) and renders a scannable QR code for whichever link is selected, plus a clickable button to open it. - -Add or remove entries in `src/lib/config/user.ts`: - -```typescript -export const links: LinkItem[] = [ - { name: 'GitHub', icon: 'mdi:github', link: 'https://github.com/SirBlobby' }, - { name: 'Email', icon: 'mdi:email', link: `mailto:${user.email}` }, - // Add page-only links here without touching user.socials -]; +```json +{ "label": "about", "icon": "mdi:account", "open": { "page": "/about" } } +{ "label": "skills.sh", "icon": "mdi:script-text", "open": { "terminal": "skills" } } +{ "label": "github", "icon": "mdi:github", "open": { "url": "https://github.com/SirBlobby" } } ``` -QR codes are generated with the `svelte-qrcode` package, loaded on the client. +`{ "terminal": "" }` opens an empty terminal. -## Blog +### Blog posts -The `/blog` page is a native markdown blog — no external blogging platform required. - -### Writing a Post - -Add a markdown file to the `blogs/` directory at the project root: +Add a markdown file to `content/blog/`. The file name is the URL slug. ```markdown --- title: My Post Title -author: Your Name +author: Sir Blob date: 2026-01-01 tags: meta, notes -excerpt: A one line summary shown on the blog list and in the RSS feed. +excerpt: One line summary for the list and the RSS feed. --- -Post content goes here, written in markdown. +Post content in markdown. ``` -Posts are picked up automatically via `import.meta.glob` in `src/lib/blog/posts.ts` — no manual registration needed. `author` and `date` are optional; `author` falls back to `user.displayname`. +The RSS feed is at `/blog/rss.xml`. -### Rendering +## Themes -`src/lib/blog/markdown.ts` is a small hand-written markdown-to-HTML renderer (headings, bold/italic, links, images, lists, blockquotes, and fenced code blocks) — no external markdown dependency. Fenced code blocks with a language tag (` ```js `, ` ```python `, etc.) get lightweight syntax highlighting via `src/lib/blog/highlight.ts`, and every code block gets a copy-to-clipboard button. +Themes live in `src/lib/theme/themes.ts` and use the same keys as a blomarchy `colors.toml`, so a theme can be copied over from the dotfiles. Each theme can set a `wallpaper` (from `static/wallpapers/`) and an `activeBorder` gradient. The default theme is set in `content/desktop.json`. -Dates are parsed and formatted in Eastern time via `src/lib/blog/date.ts`, anchored at noon UTC to avoid off-by-one day shifts from timezone conversion. +Shipped themes: Flats (default), Retro 82, Tokyo Night, Catppuccin, Arch Linux, Gruvbox, Nord, Kanagawa, Wintry, Rose, Cerberus, Catppuccin Latte, Rose Pine Dawn. -### Features +## Terminal commands -- Tags with click-to-filter on the blog list -- Full-text search across title, excerpt, and tags -- Pagination (5 posts per page) -- Previous/next post navigation on each post page -- RSS feed at `/blog/rss.xml` (`src/routes/blog/rss.xml/+server.ts`) +`help`, `whoami`, `fastfetch`, `skills`, `contact`, `email`, `experience`, `projects [category]`, `ls`, `cd `, `theme [name]`, `echo`, `cowsay`, `date`, `clear`, `exit`. Page names (`about`, `hackathons`, ...) and social names (`github`, `linkedin`, ...) also work as commands. Tab completes commands, pages after `cd`, themes after `theme` and categories after `projects`; when several match it lists them like bash. Up/Down walk history, Ctrl+L clears. -## Speed Presets +Commands live in `src/lib/terminal/commands/`. -| Preset | Effect | -|--------|--------| -| `instant` | No animation, appears immediately | -| `fast` | 3x faster than normal | -| `normal` | Default typing speed | -| `slow` | 2x slower than normal | -| `typewriter` | 3x slower, classic feel | +## Code layout -## Text Formatting +| Path | Contents | +| --- | --- | +| `src/lib/content/` | typed loader for `content/` | +| `src/lib/blog/` | markdown renderer, frontmatter, highlighting, dates | +| `src/lib/theme/` | theme palettes, stylesheet generation, current theme state | +| `src/lib/desktop/` | window manager, dwindle tiling, keybinds, desktop actions | +| `src/lib/terminal/` | terminal output model and commands | +| `src/lib/site/pages.ts` | page list used by the terminal and launcher | +| `src/lib/components/shell/` | bar, workspaces, clock, theme menu, Discord status | +| `src/lib/server/discord.ts` | Discord bot and presence lookup | +| `src/lib/components/desktop/` | desktop, windows, icons, launcher | +| `src/lib/components/terminal/` | kitty terminal | +| `src/lib/components/ui/` | shared page pieces | +| `src/routes/` | pages | -Use inline formatting with the `(&specs)text(&)` syntax: +## Discord status -### Colors +The bar shows live Discord presence (status, avatar, guild tag, activities). A discord.js bot starts with the server (`src/hooks.server.ts`) and the bar polls `/api/discord` every 30 seconds. Copy `.env.example` to `.env` and fill in: -```typescript -// Basic colors -'(&red)Red text(&)' -'(&green)Green text(&)' -'(&blue)Blue text(&)' -'(&yellow)Yellow text(&)' -'(&magenta)Magenta text(&)' -'(&cyan)Cyan text(&)' -'(&orange)Orange text(&)' -'(&pink)Pink text(&)' -'(&gray)Gray text(&)' -'(&white)White text(&)' +| Variable | Value | +| --- | --- | +| `DISCORD_TOKEN` | bot token, with the Server Members and Presence intents enabled | +| `DISCORD_USER_ID` | your user id | +| `DISCORD_GUILD_ID` | a server both you and the bot are in | -// Semantic colors (theme-aware) -'(&primary)Primary color(&)' -'(&accent)Accent color(&)' -'(&muted)Muted text(&)' -'(&error)Error text(&)' -'(&success)Success text(&)' -'(&warning)Warning text(&)' -'(&info)Info text(&)' +Variables are declared in `src/env.ts`. Without them the widget shows your profile name as offline. -// Custom hex colors -'(&#ff6b6b)Custom color(&)' -``` +## Analytics -### Background Colors - -Add `bg-` prefix to any color: - -```typescript -'(&bg-red)Red background(&)' -'(&bg-blue,white)Blue bg with white text(&)' -'(&bg-surface)Surface background(&)' -'(&bg-#333333)Custom bg color(&)' -``` - -### Text Styles - -```typescript -'(&bold)Bold text(&)' -'(&italic)Italic text(&)' -'(&dim)Dimmed text (60% opacity)(&)' -'(&underline)Underlined text(&)' -'(&strikethrough)Strikethrough text(&)' -'(&strike)Strikethrough shorthand(&)' -'(&overline)Overlined text(&)' -``` - -### Inline Icons - -```typescript -'(&icon, mdi:github) GitHub' // Inline icon, default size -'(&icon, mdi:trophy, 32) Winner' // Inline icon with a custom size in pixels -``` - -### Combining Styles - -Combine multiple styles with commas: - -```typescript -'(&bold,red)Bold red text(&)' -'(&italic,cyan,underline)Italic cyan underlined(&)' -'(&bg-blue,white,bold)Bold white on blue(&)' -'(&dim,strikethrough,gray)Dim gray strikethrough(&)' -``` - -## Line Types - -### Basic Lines - -```typescript -const lines: TerminalLine[] = [ - { type: 'command', content: 'ls -la' }, // With prompt prefix - { type: 'output', content: 'File listing...' }, // Muted text - { type: 'error', content: 'Error message' }, // Red with prefix - { type: 'success', content: 'Success!' }, // Green with prefix - { type: 'info', content: 'Information' }, // Primary with prefix - { type: 'header', content: 'Section Title' }, // Bold heading - { type: 'blank', content: '' }, // Empty line - { 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 -{ - type: 'button', - content: 'Click me', - icon: 'mdi:github', // Iconify icon - 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 - border: false, // Disable default border (default: true) - flex: true, // Grow to fill available space in a row group (default: false) -} -``` - -### 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 -{ - type: 'link', - content: 'Visit GitHub', - icon: 'mdi:github', // Optional icon - style: 'accent', // Styling - href: 'https://github.com', - external: true, // Open in new tab (auto-detected for http/https) -} -``` - -### Image - -```typescript -{ - type: 'image', - content: 'Caption text', // Optional caption - image: '/path/to/image.png', - imageAlt: 'Alt text', - imageWidth: 300, // Max width in pixels -} -``` - -### Card - -```typescript -{ - type: 'card', - content: 'Card body text with (&bold)formatting(&) support', - cardTitle: 'Card Title', // Optional header - cardFooter: 'Footer text', // Optional footer - icon: 'mdi:star', // Optional header icon - image: '/path/to/image.png', // Optional card image - style: 'primary', // Border accent color - display: 'flex', // flex | grid | block (controls children layout) - children: [ // Optional nested elements - { type: 'button', content: 'Action', style: 'primary' } - ], - cardWidth: '1/2', // Width (string fraction, decimal, or px) - cardHeight: 300, // Height (string fraction, decimal, or px) - cardFloat: 'center', // start | center | end -} -``` - -### Progress Bar - -```typescript -{ - type: 'progress', - content: 'Loading assets...', // Label above bar - progress: 75, // 0-100 percentage - progressLabel: '75%', // Custom label (defaults to percentage) - style: 'accent', // Bar color -} -``` - -### Accordion - -```typescript -{ - type: 'accordion', - content: '', - accordionItems: [ - { title: 'Section 1', content: 'Content for section 1 with (&cyan)colors(&)' }, - { title: 'Section 2', content: 'Content for section 2' }, - ], - accordionOpen: true, // First item open by default - style: 'primary', // Accent color -} -``` - -### Table - -```typescript -{ - type: 'table', - content: 'Table Title', // Optional title - tableHeaders: ['Name', 'Role', 'Status'], - tableRows: [ - ['Alice', 'Developer', '(&success)Active(&)'], - ['Bob', 'Designer', '(&warning)Away(&)'], - ], - style: 'accent', // Header color -} -``` - -### Tooltip - -```typescript -{ - type: 'tooltip', - content: 'Hover me', // Trigger text - tooltipText: 'This is helpful information!', - tooltipPosition: 'top', // top | bottom | left | right - style: 'info', // Tooltip border color -} -``` - -### Group - -Groups allow you to arrange multiple elements together with custom layout: - -```typescript -{ - type: 'group', - content: '', - groupDirection: 'row', // row | column (default: row) - groupAlign: 'start', // start | center | end - groupGap: '1rem', // CSS gap value - groupExpand: true, // Expand children to fill width (default: false) - inline: true, // Render inline with other elements - children: [ - { type: 'output', content: 'Label:', inline: true }, - { type: 'button', content: 'Action', style: 'primary', inline: true }, - { type: 'link', content: 'More info', href: '/help', inline: true } - ] -} -``` - -Groups support nested groups and all element types as children. Children are rendered using the same `TuiLine` component, ensuring consistent behavior. - -## TUI Components - -### TerminalTUI - -Main terminal component: - -```svelte - 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) -- `enableCommands` - Show the interactive command input (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. - -## Terminal API - -The `TerminalTUI` component exposes a reactive API for programmatic control via the `terminal` bindable prop. - -### Setup - -```svelte - - - -``` - -### API Methods - -#### Writing Lines -| Method | Description | -|--------|-------------| -| `terminal.write(line)` | Append a single line | -| `terminal.writeLines(lines)` | Append multiple lines | -| `terminal.clear()` | Remove all lines | -| `terminal.setLines(lines)` | Replace all lines | - -#### Updating Lines -| Method | Description | -|--------|-------------| -| `terminal.update(index, updates)` | Update line by index with partial changes | -| `terminal.updateContent(index, content)` | Update just the content of a line | -| `terminal.updateById(id, updates)` | Update line by its `id` property | - -#### Removing Lines -| Method | Description | -|--------|-------------| -| `terminal.remove(index)` | Remove line at index | -| `terminal.removeRange(start, count)` | Remove a range of lines | -| `terminal.removeById(id)` | Remove line by its `id` property | - -#### Inserting Lines -| Method | Description | -|--------|-------------| -| `terminal.insert(index, line)` | Insert line at a specific index | - -#### Reading State -| Method | Description | -|--------|-------------| -| `terminal.getLine(index)` | Get line at index | -| `terminal.getLines()` | Get all lines (copy) | -| `terminal.getLineCount()` | Get number of lines | -| `terminal.findById(id)` | Find index of line by id | -| `terminal.isAnimating()` | Check if typing animation is active | - -#### Navigation & Control -| Method | Description | -|--------|-------------| -| `terminal.scrollToBottom()` | Scroll to bottom of terminal | -| `terminal.scrollToLine(index)` | Scroll to specific line | -| `terminal.skip()` | Skip current typing animation | -| `terminal.restart()` | Restart typing animation from beginning | - -### Example: Dynamic Updates - -```svelte - - - - -``` - -## 3D Model Viewer - -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 -- Graceful Fallback: Shows an error message when WebGL is unavailable instead of crashing - -### Usage - -```svelte - -``` - -Place `.glb` files in `/static/models/` and they'll be accessible at `/models/filename.glb`. - -## Tech Stack - -- Framework: SvelteKit 2.x with Svelte 5 runes -- Styling: Tailwind CSS 4.x -- 3D: Three.js with GLTFLoader -- QR codes: svelte-qrcode -- Icons: @iconify/svelte -- Font: JetBrains Mono -- Runtime: Bun - -## Development - -```bash -# Install dependencies -bun install - -# Start dev server -bun run dev - -# Build for production -bun run build - -# Type-check -bun run check -``` - -## Keyboard Shortcuts - -| Key | Action | -|-----|--------| -| Up / k | Navigate up | -| Down / j | Navigate down | -| Enter | Activate button | -| Y | Skip typing animation | -| T | Toggle dark/light mode | - -When the command input is focused, these keys type normally instead of triggering navigation. - -### 3D Model Viewer - -| Key | Action | -|-----|--------| -| Left | Rotate camera left | -| Right | Rotate camera right | -| Up | Rotate camera up | -| Down | 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. Shipped themes: Arch, Catppuccin, Wintry, Rose, and Cerberus (`src/lib/stores/theme.ts`). - -### Theme File Structure - -```json -{ - "name": "Theme Name", - "icon": "arch", - "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": { }, - "colorMap": { } - } -} -``` - -### 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 - -### 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 +Umami is loaded in `src/app.html`. diff --git a/content/about.json b/content/about.json new file mode 100644 index 0000000..9a4f934 --- /dev/null +++ b/content/about.json @@ -0,0 +1,28 @@ +{ + "hobbies": [ + { "name": "Swimming", "icon": "proicons:person-swim", "description": "I enjoy swimming as a way to relax and stay active. It is my favorite sport." }, + { "name": "Baking", "icon": "proicons:cake", "description": "I love baking pastries and experimenting with new dessert recipes." }, + { "name": "Robotics", "icon": "proicons:robot", "description": "Building and programming robots to solve problems is fun." } + ], + "games": [ + { "name": "Minecraft", "image": "/img/minecraft.png", "url": "https://minecraft.net" }, + { "name": "Pokemon TCG Online", "image": "/img/pokemontcg.png", "url": "https://www.pokemon.com/us/pokemon-tcg" }, + { "name": "Fire Watch", "image": "/img/firewatch.png", "url": "https://firewatchgame.com" }, + { "name": "Expedition 33", "image": "/img/e33.png", "url": "https://expedition33.com" }, + { "name": "Mini Motorways", "image": "/img/mini.png", "url": "https://store.steampowered.com/app/1127500/Mini_Motorways/" }, + { "name": "Risk", "image": "/img/risk.png", "url": "https://store.steampowered.com/app/1128810/RISK_Global_Domination/" }, + { "name": "Slay the Spire", "image": "/img/slay_the_spire.png", "url": "https://store.steampowered.com/app/646570/Slay_the_Spire/" }, + { "name": "Slay the Spire 2", "image": "/img/slay_the_spire_2.png", "url": "https://store.steampowered.com/app/2868840/Slay_the_Spire_2/" } + ], + "shows": [ + { "name": "Silo", "rating": 10, "url": "https://www.imdb.com/title/tt14688458/" }, + { "name": "D.P. (KDrama)", "rating": 10, "url": "https://www.imdb.com/title/tt13024974/" }, + { "name": "Andor", "rating": 9, "url": "https://www.imdb.com/title/tt9253284/" }, + { "name": "Severance", "rating": 9, "url": "https://www.imdb.com/title/tt11280740/" }, + { "name": "The Mandalorian", "rating": 7, "url": "https://www.imdb.com/title/tt8111088/" } + ], + "movies": [ + { "name": "Interstellar", "rating": 10, "url": "https://www.imdb.com/title/tt0816692/" }, + { "name": "How to Train Your Dragon", "rating": 10, "url": "https://www.imdb.com/title/tt1621800/" } + ] +} diff --git a/content/blog.json b/content/blog.json new file mode 100644 index 0000000..3363335 --- /dev/null +++ b/content/blog.json @@ -0,0 +1,5 @@ +{ + "name": "Finding Out", + "tagline": "My Engineering Blog", + "postsPerPage": 5 +} diff --git a/content/desktop.json b/content/desktop.json new file mode 100644 index 0000000..f8b21da --- /dev/null +++ b/content/desktop.json @@ -0,0 +1,28 @@ +{ + "defaultTheme": "flats", + "workspaces": [ + { "number": 1, "name": "home", "path": "/" }, + { "number": 2, "name": "about", "path": "/about" }, + { "number": 3, "name": "projects", "path": "/projects" }, + { "number": 4, "name": "blog", "path": "/blog" }, + { "number": 5, "name": "links", "path": "/links" }, + { "number": 6, "name": "portfolio", "path": "/portfolio" }, + { "number": 7, "name": "models", "path": "/models" } + ], + "icons": [ + { "label": "about", "icon": "ph:user-circle-duotone", "open": { "page": "/about" } }, + { "label": "portfolio", "icon": "ph:briefcase-duotone", "open": { "page": "/portfolio" } }, + { "label": "projects", "icon": "ph:folder-open-duotone", "open": { "page": "/projects" } }, + { "label": "hackathons", "icon": "ph:trophy-duotone", "open": { "page": "/projects/hackathons" } }, + { "label": "blog", "icon": "ph:notebook-duotone", "open": { "page": "/blog" } }, + { "label": "models", "icon": "ph:cube-duotone", "open": { "page": "/models" } }, + { "label": "links", "icon": "ph:qr-code-duotone", "open": { "page": "/links" } }, + { "label": "kitty", "icon": "ph:terminal-window-duotone", "open": { "terminal": "" } }, + { "label": "whoami.sh", "icon": "ph:identification-card-duotone", "open": { "terminal": "whoami" } }, + { "label": "fastfetch.sh", "icon": "ph:cpu-duotone", "open": { "terminal": "fastfetch" } }, + { "label": "skills.sh", "icon": "ph:lightning-duotone", "open": { "terminal": "skills" } }, + { "label": "contact.sh", "icon": "ph:envelope-simple-duotone", "open": { "terminal": "contact" } }, + { "label": "github", "icon": "ph:github-logo-duotone", "open": { "url": "https://github.com/SirBlobby" } } + ], + "startupCommand": "fastfetch" +} diff --git a/content/experience.json b/content/experience.json new file mode 100644 index 0000000..5e4bc67 --- /dev/null +++ b/content/experience.json @@ -0,0 +1,54 @@ +[ + { + "role": "Undergraduate Research Assistant, CREATE Lab", + "organization": "George Mason University, College of Engineering and Computing", + "period": "May 2026 – Present", + "highlights": [ + "Built Astroswarm, a visual coding game that turns swarm control into a playable simulator, generating trial data at scale on which algorithms best defend a target.", + "Researching swarm robotics in Python and ROS for a research program on controlling robot fleets with one operator." + ], + "tech": ["Python", "ROS", "Swarm Robotics"], + "links": [ + { "label": "Profile", "url": "https://create.autonomousrobotics.club/team/gagan-manjunatha" }, + { "label": "Astroswarm", "url": "https://astroswarm.autonomousrobotics.club/" }, + { "label": "GMU CEC News", "url": "https://cec.gmu.edu/news/2026-07/new-art-herding-robots" } + ] + }, + { + "role": "AI Summer Camp Instructor", + "organization": "ACCESS Academy", + "period": "Jul 2026 – Aug 2026", + "highlights": [ + "Taught the robotics section of an AI camp for 60 rising 9th to 12th graders, guiding teams to build robot kits and design algorithms that autonomously navigated a maze." + ], + "tech": ["Robotics", "Teaching"], + "links": [{ "label": "Program", "url": "https://accessacademy.cehd.gmu.edu/ai-summer-camp" }] + }, + { + "role": "Lead Software Engineer", + "organization": "Raytheon Autonomous Vehicle Competition", + "period": "Dec 2025 – May 2026", + "highlights": [ + "Beat four competing universities including Virginia Tech.", + "Led development of an autonomous system in Python and ROS2, enabling a UAV to scout and land on a moving UGV in an environment without GPS, validated against ArUco marker detection in Gazebo simulation." + ], + "tech": ["Python", "ROS2", "Gazebo", "Computer Vision"], + "awards": ["1st Place Overall Winner"], + "links": [ + { "label": "GMU News", "url": "https://www.gmu.edu/news/2026-05/george-mason-students-fly-first-place-no-gps-no-grade-and-almost-no-problems" }, + { "label": "Video", "url": "https://www.youtube.com/watch?v=J9QkS8nLYy0" } + ] + }, + { + "role": "QA Automation Intern", + "organization": "Deltek", + "employmentType": "Internship · Hybrid", + "period": "Jun 2025 – Aug 2025", + "highlights": [ + "Designed a workflow integrating AI into the QA automation pipeline for the GovWin IQ team.", + "Researched Model Context Protocol servers, publishing a public Selenium MCP package to NPM for agentic browser automation." + ], + "tech": ["AI", "TypeScript", "Selenium", "MCP"], + "links": [{ "label": "NPM Package", "url": "https://www.npmjs.com/package/@sirblob/mcp-selenium" }] + } +] diff --git a/content/leadership.json b/content/leadership.json new file mode 100644 index 0000000..21390ec --- /dev/null +++ b/content/leadership.json @@ -0,0 +1,6 @@ +[ + { "role": "Team Lead", "organization": "GMU RAVC/ARC Team", "url": "https://gmu.autonomousrobotics.club/" }, + { "role": "President", "organization": "GMU Minecraft Club", "url": "https://gmmc.sirblob.co/" }, + { "role": "Eagle Scout", "organization": "BSA" }, + { "role": "Volunteer", "organization": "Fairfax County CERT" } +] diff --git a/content/models.json b/content/models.json new file mode 100644 index 0000000..1c764d6 --- /dev/null +++ b/content/models.json @@ -0,0 +1,16 @@ +[ + { + "name": "Bake Potato", + "description": "A fun baked-potato 3D model for demos and testing.", + "file": "/models/BakePotato.glb", + "software": ["Blender"], + "polyCount": "1k tris" + }, + { + "name": "Soda Can", + "description": "A realistic soda can GLB used for materials/tests.", + "file": "/models/Soda.glb", + "software": ["Blender"], + "polyCount": "800 tris" + } +] diff --git a/content/profile.json b/content/profile.json new file mode 100644 index 0000000..09bc01e --- /dev/null +++ b/content/profile.json @@ -0,0 +1,29 @@ +{ + "name": "Gagan Manjunatha", + "displayName": "Sir Blob", + "username": "sirblob", + "hostname": "engineering", + "title": "Computer Science Student", + "email": "sirblob0@gmail.com", + "avatar": "/blob_nerd.png", + "bio": "An engineer who loves making things. I build fun coding projects, compete in hackathons and robotics competitions, and enjoy games like Minecraft and Pokémon TCG Live. I'm interested in Open Source, Robotics, Embedded Systems, and AI/ML.", + "tagline": "My passion is using Computer Science with practical Engineering to enhance technology for the future.", + "education": { + "school": "George Mason University", + "degree": "Bachelor of Science, Computer Science", + "graduation": "Expected June 2028" + }, + "socials": [ + { "name": "GitHub", "icon": "mdi:github", "url": "https://github.com/SirBlobby" }, + { "name": "LinkedIn", "icon": "mdi:linkedin", "url": "https://www.linkedin.com/in/gmanjunatha/" }, + { "name": "Devpost", "icon": "simple-icons:devpost", "url": "https://devpost.com/Sir_Blob_" }, + { "name": "Discord", "icon": "ic:baseline-discord", "url": "https://discord.com/users/sir_blob_" }, + { "name": "Email", "icon": "mdi:email", "url": "mailto:sirblob0@gmail.com" } + ], + "skills": { + "Languages": ["Python", "TypeScript/JavaScript", "C/C++", "Java", "Go", "Rust", "SQL"], + "Frameworks": ["React/Next.js", "SvelteKit", "Node/Express", "Flask", "Tailwind CSS", "ROS2", "Gazebo"], + "Tools & Data": ["Docker", "Git", "Linux/Windows Server", "PostgreSQL", "MySQL", "SQLite", "MongoDB", "Redis"] + }, + "interests": ["Open Source", "Robotics", "Embedded Systems", "Game Development", "AI/ML"] +} diff --git a/content/projects/hackathons.json b/content/projects/hackathons.json new file mode 100644 index 0000000..9dc2eed --- /dev/null +++ b/content/projects/hackathons.json @@ -0,0 +1,85 @@ +[ + { + "name": "HushMap", + "description": "Architected a real-time campus noise and occupancy monitoring system utilizing SvelteKit, FastAPI, and MongoDB to help students locate quiet study spaces.", + "image": "/hacks/hushmap.png", + "tags": ["Svelte", "YOLOv8", "FastAPI", "MongoDB"], + "featured": true, + "event": { "name": "Bitcamp 2026", "university": "University of Maryland", "location": "College Park, MD", "year": "2026" }, + "awards": [{ "track": "Best UI/UX Hack", "place": "Winner" }], + "links": { "demo": "https://hushmap.study/", "devpost": "https://devpost.com/software/hushmap-193nia" } + }, + { + "name": "LearningBuddy", + "description": "ESP32-S3 IoT study companion in C++ that streams audio over WebSockets with low latency for live lecture capture, with transcription and MongoDB indexing behind a searchable \"Desk Pet\" dashboard.", + "image": "/hacks/learningbuddy.png", + "tags": ["C++", "ESP32-S3", "WebSocket", "MongoDB", "IoT"], + "featured": true, + "event": { "name": "HackFax x PatriotHacks 2026", "university": "George Mason University", "location": "Fairfax, VA", "year": "2026" }, + "awards": [{ "track": "Best use of MongoDB", "place": "Winner" }], + "links": { "code": "https://github.com/SirBlobby/LearningBuddy", "devpost": "https://devpost.com/software/studybuddy-bc0i3j" } + }, + { + "name": "Carbin", + "description": "Encourage student participation in responsible waste management with smart bins that guide proper disposal.", + "image": "/hacks/carbin.png", + "tags": ["AI", "Azure", "CloudConvert", "Python", "React", "TypeScript", "API"], + "featured": true, + "event": { "name": "PatriotHacks 2024", "university": "George Mason University", "location": "Fairfax, VA", "year": "2024" }, + "awards": [ + { "track": "Save the World", "place": "2nd Place" }, + { "track": "Microsoft X Cloudforce", "place": "2nd Place" } + ], + "links": { "code": "https://github.com/SirBlobby/patriotHacks2024", "devpost": "https://devpost.com/software/carboniferous-akc4mj" } + }, + { + "name": "Ethix", + "description": "A sustainability platform that helps users detect and report greenwashing, misleading environmental claims made by companies and products.", + "image": "/img/hoya26.png", + "tags": ["Svelte", "TypeScript", "Python", "Flask", "Tailwind", "Sustainability"], + "featured": true, + "event": { "name": "HoyaHax 2026", "university": "Georgetown University", "location": "Washington, D.C.", "year": "2026" }, + "links": { "demo": "https://ethix.sirblob.co/", "code": "https://github.com/SirBlobby/Hoya26", "devpost": "https://devpost.com/software/ethix" } + }, + { + "name": "Food Decisive", + "description": "Ever felt the indecisiveness of choosing what to eat? Don't fret! Decide your next bite with Food Decisive.", + "image": "/hacks/fooddecisive.png", + "tags": ["AI", "Llama3.1", "Svelte", "Node.js"], + "demoMayBeOffline": true, + "event": { "name": "VTHacks 12", "university": "Virginia Tech", "location": "Blacksburg, VA", "year": "2024" }, + "links": { "demo": "https://fooddecisive.sirblob.co/", "code": "https://github.com/SirBlobby/VTHacks-12", "devpost": "https://devpost.com/software/food-decisive" } + }, + { + "name": "PatSafe", + "description": "Bridging the gap between doctors and patients for seamless post-discharge care.", + "image": "/hacks/patsafe.png", + "tags": ["Javascript", "Next.js", "React", "TypeScript", "LangChain", "OpenAI"], + "event": { "name": "HoyaHax 2025", "university": "Georgetown University", "location": "Washington, D.C.", "year": "2025" }, + "links": { "demo": "https://hoya-hax2025.vercel.app/", "code": "https://github.com/SirBlobby/HoyaHax2025", "devpost": "https://devpost.com/software/patsafe" } + }, + { + "name": "Fauxcall", + "description": "This product is perfect for situations where you are walking at night and you feel unsafe from someone. Fauxcall lets users create a convincing fake phone call to deter potential attackers and provide a quick escape mechanism.", + "tags": ["MongoDB", "Next.js", "Python", "React", "Sesame", "Skeleton", "Twilio"], + "event": { "name": "HooHacks 2025", "university": "University of Virginia", "location": "Charlottesville, VA", "year": "2025" }, + "links": { "code": "https://github.com/SirBlobby/HooHacks-12", "devpost": "https://devpost.com/software/fauxcall" } + }, + { + "name": "Drink Happy", + "description": "drinkhappy.tech is a gamified hydration wellness app that helps users track drinks, earn points for healthy choices, and compete with friends. It's powered by Gemini AI for smart drink recognition.", + "image": "/hacks/drinkhappy.png", + "tags": ["API", "Auth0", "Gemini", "JavaScript", "MongoDB", "Next.js", "React", "Tailwind", "Vercel"], + "event": { "name": "Bitcamp 2025", "university": "University of Maryland", "location": "College Park, MD", "year": "2025" }, + "links": { "demo": "https://drinkhappy.tech", "code": "https://github.com/SirBlobby/Bitcamp-2025", "devpost": "https://devpost.com/software/drink-happy" } + }, + { + "name": "Roadcast", + "description": "Roadcast provides drivers with crash data and weather insights to choose safer routes home. The project combines historical crash data, weather feeds, and spatial analysis to surface hazardous areas and route-level safety recommendations.", + "image": "/hacks/roadcast.png", + "tags": ["React", "Node.js", "Python", "MongoDB", "Geospatial", "Mapbox"], + "demoMayBeOffline": true, + "event": { "name": "VTHacks 13", "university": "Virginia Tech", "location": "Blacksburg, VA", "year": "2025" }, + "links": { "demo": "https://roadcast.sirblob.co/", "code": "https://github.com/SirBlobby/roadcast", "devpost": "https://devpost.com/software/roadcast" } + } +] diff --git a/content/projects/opensource.json b/content/projects/opensource.json new file mode 100644 index 0000000..adcdfec --- /dev/null +++ b/content/projects/opensource.json @@ -0,0 +1,36 @@ +[ + { + "name": "TypstDrive", + "description": "Self hosted collaborative Typst editor with a Rust backend, SvelteKit frontend, real time multi user editing via Yjs, and a public render API.", + "version": "2.2.0", + "period": "2026 – Present", + "featured": true, + "tags": ["Rust", "SvelteKit", "Yjs", "Typst", "Self-Hosted"], + "image": "/projects/typstdrive.png", + "links": { "code": "https://github.com/SirBlobby/TypstDrive" } + }, + { + "name": "Typst Desktop", + "description": "Tauri desktop client for TypstDrive that keeps the same documents as local files and syncs them back with a three way merge.", + "version": "2.2.0", + "period": "2026 – Present", + "featured": true, + "tags": ["Tauri", "Rust", "Typst", "Offline Sync"], + "image": "/projects/typstdesktop.png", + "links": { "code": "https://github.com/SirBlobby/typst-desktop" } + }, + { + "name": "Pkit", + "description": "CLI toolkit and utilities (Rust project), a small developer-focused CLI.", + "tags": ["Rust", "CLI"], + "image": "https://icons.veryicon.com/png/o/business/vscode-program-item-icon/rust-1.png", + "links": { "code": "https://github.com/dead-projects-inc/pkit-cli", "demo": "https://pkit.sirblob.co/" } + }, + { + "name": "OBookLLM", + "description": "OBookLLM is a powerful, self-hosted, offline-capable alternative to NotebookLLM.", + "tags": ["Python", "LLM", "AI", "NotebookLLM"], + "image": "https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg", + "links": { "code": "https://github.com/SirBlobby/OBookLLM" } + } +] diff --git a/content/projects/packages.json b/content/projects/packages.json new file mode 100644 index 0000000..2faafd7 --- /dev/null +++ b/content/projects/packages.json @@ -0,0 +1,21 @@ +[ + { + "name": "MCP Selenium", + "description": "Selenium automation utilities for MCP workflows, packaged for reuse.", + "tags": ["TypeScript", "Selenium"], + "featured": true, + "links": { "package": "https://www.npmjs.com/package/@sirblob/mcp-selenium" } + }, + { + "name": "PokemonTCGAPI", + "description": "Unofficial NPM package wrapper for the PokemonTCG API.", + "tags": ["TypeScript", "Node.js"], + "links": { "package": "https://www.npmjs.com/package/@bosstop/pokemontcgapi" } + }, + { + "name": "MCSS TS API", + "description": "TypeScript API for MC Server Soft services, published to NPM.", + "tags": ["TypeScript", "Node.js"], + "links": { "package": "https://www.npmjs.com/package/@mcserversoft/mcss-api" } + } +] diff --git a/content/projects/research.json b/content/projects/research.json new file mode 100644 index 0000000..658c57f --- /dev/null +++ b/content/projects/research.json @@ -0,0 +1,14 @@ +[ + { + "name": "Astroswarm", + "organization": "CREATE Lab, George Mason University", + "period": "May 2026 – Present", + "description": "A visual coding game that turns swarm control into a playable simulator, generating trial data at scale on which algorithms best defend a target. Part of a research program on controlling robot fleets with one operator.", + "tags": ["Python", "ROS", "Swarm Robotics"], + "featured": true, + "links": { + "demo": "https://astroswarm.autonomousrobotics.club/", + "article": "https://cec.gmu.edu/news/2026-07/new-art-herding-robots" + } + } +] diff --git a/package.json b/package.json index 3a920cf..ac2dc2a 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,40 @@ { - "name": "website", + "name": "new", "private": true, - "version": "7.0.0", + "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite dev --host", + "dev": "vite dev", "build": "vite build", - "server": "bun server/server.js", - "start": "vite build && bun server/server.js" + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { - "@sveltejs/adapter-node": "^5.5.7", - "@sveltejs/kit": "^2.70.1", - "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@sveltejs/adapter-node": "^6.0.0", + "@sveltejs/kit": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.20", - "@tailwindcss/vite": "^4.3.3", - "svelte": "^5.56.8", - "svelte-check": "^4.7.4", - "tailwindcss": "^4.3.3", - "typescript": "^5.9.3", - "vite": "^7.3.6" + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^24", + "svelte": "^5.57.1", + "svelte-check": "^4.6.0", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", + "vite": "^8.3.0" }, "dependencies": { - "@iconify/svelte": "^5.2.2", - "@threlte/core": "^8.5.16", - "@types/three": "^0.185.1", - "cors": "^2.8.6", + "@iconify/svelte": "^5.2.3", + "@types/three": "^0.185.4", "discord.js": "^14.27.0", - "dotenv": "^17.4.2", - "express": "^5.2.1", - "hotkeys-js": "^4.0.4", - "play-dl": "^1.9.7", "svelte-qrcode": "^1.0.1", "three": "^0.185.1" + }, + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*", + "#content/*": "./content/*" } } diff --git a/server/server.js b/server/server.js deleted file mode 100644 index aa9e379..0000000 --- a/server/server.js +++ /dev/null @@ -1,18 +0,0 @@ -import { handler } from '../build/handler.js'; -import dotenv from 'dotenv'; -import http from 'http'; -import express from 'express'; -import cors from 'cors'; - -dotenv.config(); - -const app = express(); -const server = http.Server(app); - -app.use(cors()); - -app.use(handler); - -server.listen(process.env.PORT, () => { - console.log('listening on port http://localhost:' + process.env.PORT); -}); \ No newline at end of file diff --git a/src/app.html b/src/app.html index 9991327..14eef7c 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,13 @@ + + %sveltekit.head% diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..ae3b611 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,18 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +const optional = (value: string | undefined) => value || undefined; + +export const variables = defineEnvVars({ + DISCORD_TOKEN: { + schema: optional, + description: 'Discord bot token used to read presence' + }, + DISCORD_USER_ID: { + schema: optional, + description: 'Discord user whose status is shown in the bar' + }, + DISCORD_GUILD_ID: { + schema: optional, + description: 'Guild shared by the bot and the user, needed to read presence' + } +}); diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..3498a90 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,6 @@ +import type { ServerInit } from '@sveltejs/kit'; +import { getDiscordClient } from '#lib/server/discord'; + +export const init: ServerInit = () => { + getDiscordClient(); +}; diff --git a/src/lib/assets/css/background-3d.css b/src/lib/assets/css/background-3d.css deleted file mode 100644 index f52e45a..0000000 --- a/src/lib/assets/css/background-3d.css +++ /dev/null @@ -1,14 +0,0 @@ -.scene-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; - pointer-events: none; - background-color: var(--scene-bg); -} - -.scene-container :global(canvas) { - display: block; -} diff --git a/src/lib/assets/css/index.css b/src/lib/assets/css/index.css deleted file mode 100644 index 0f2b2a9..0000000 --- a/src/lib/assets/css/index.css +++ /dev/null @@ -1,34 +0,0 @@ -/* TUI Components CSS Index */ -/* This file can be imported to get all TUI component styles at once */ - -/* TUI Core Components */ -@import './tui-header.css'; -@import './tui-footer.css'; -@import './tui-body.css'; - -/* TUI Display Components */ -@import './tui-button.css'; -@import './tui-link.css'; -@import './tui-card.css'; -@import './tui-card-grid.css'; -@import './tui-progress.css'; -@import './tui-accordion.css'; -@import './tui-table.css'; -@import './tui-tooltip.css'; - -/* TUI Form Components */ -@import './tui-input.css'; -@import './tui-textarea.css'; -@import './tui-checkbox.css'; -@import './tui-radio.css'; -@import './tui-select.css'; -@import './tui-toggle.css'; - -/* Main Components */ -@import './terminal.css'; -@import './terminal-tui.css'; -@import './terminal-page.css'; -@import './model-viewer.css'; -@import './navbar.css'; -@import './navbar-waybar.css'; -@import './background-3d.css'; diff --git a/src/lib/assets/css/model-viewer.css b/src/lib/assets/css/model-viewer.css deleted file mode 100644 index cb2dc9a..0000000 --- a/src/lib/assets/css/model-viewer.css +++ /dev/null @@ -1,172 +0,0 @@ -.model-viewer { - display: flex; - flex-direction: column; - background: var(--viewer-bg); - border: 1px solid var(--viewer-border); - border-radius: 8px; - overflow: hidden; - transition: all 0.3s ease; -} - -.model-viewer.fullscreen { - position: fixed; - inset: 60px 0 0 0; - z-index: 100; - border-radius: 0; - border: none; -} - -.viewer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0.75rem; - background: color-mix(in srgb, var(--viewer-bg) 80%, black); - border-bottom: 1px solid var(--viewer-border); -} - -.header-left { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--viewer-primary); - font-size: 0.85rem; - font-weight: 500; -} - -.header-controls { - display: flex; - gap: 0.25rem; -} - -.control-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: transparent; - border: 1px solid transparent; - border-radius: 4px; - color: var(--viewer-muted); - cursor: pointer; - transition: all 0.2s ease; -} - -.control-btn:hover { - background: var(--viewer-border); - color: var(--viewer-text); - border-color: var(--viewer-border); -} - -.control-btn.active { - background: var(--viewer-primary); - color: var(--viewer-bg); - border-color: var(--viewer-primary); -} - -.control-btn.small { - width: 24px; - height: 24px; -} - -/* Extended controls panel */ -.controls-panel { - display: flex; - flex-wrap: wrap; - gap: 1rem; - padding: 0.5rem 0.75rem; - background: color-mix(in srgb, var(--viewer-bg) 90%, black); - border-bottom: 1px solid var(--viewer-border); -} - -.control-group { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.control-label { - font-size: 0.7rem; - color: var(--viewer-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.control-buttons { - display: flex; - align-items: center; - gap: 0.25rem; -} - -.control-value { - font-size: 0.7rem; - color: var(--viewer-text); - min-width: 2.5rem; - text-align: center; - font-family: 'JetBrains Mono', monospace; -} - -.canvas-container { - flex: 1; - min-height: 300px; - position: relative; - background: radial-gradient( - circle at center, - 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) { - display: block; -} - -.loading-overlay, -.error-overlay { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.75rem; - color: var(--viewer-muted); - font-size: 0.9rem; -} - -.error-overlay { - color: #f38ba8; -} - -:global(.spin) { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.viewer-footer { - display: flex; - align-items: center; - justify-content: center; - gap: 1.5rem; - padding: 0.5rem; - background: color-mix(in srgb, var(--viewer-bg) 80%, black); - border-top: 1px solid var(--viewer-border); -} - -.hint { - display: flex; - align-items: center; - gap: 0.35rem; - color: var(--viewer-muted); - font-size: 0.75rem; -} diff --git a/src/lib/assets/css/navbar-waybar.css b/src/lib/assets/css/navbar-waybar.css deleted file mode 100644 index dde24d9..0000000 --- a/src/lib/assets/css/navbar-waybar.css +++ /dev/null @@ -1,808 +0,0 @@ -/* Waybar main container */ -.waybar { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; - height: var(--navbar-height); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 0.75rem; - background: var(--bar-bg); - border-bottom: 1px solid var(--bar-border); - font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 0.8rem; -} - -/* Bar sections */ -.bar-left, -.bar-center, -.bar-right { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.bar-left { - flex: 1; -} - -.bar-right { - flex: 1; - justify-content: flex-end; -} - -/* Desktop-only items */ -.desktop-only { - display: flex; -} - -.workspaces.desktop-only, -.window-title.desktop-only { - display: flex; -} - -@media (max-width: 768px) { - .desktop-only, - .workspaces.desktop-only, - .window-title.desktop-only { - display: none !important; - } - - .bar-left { - gap: 0.35rem; - } - - .bar-center { - display: none; - } -} - -/* Module base style */ -.module { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.35rem 0.6rem; - background: var(--bar-bg-module); - border-radius: 6px; - transition: all 0.15s ease; -} - -/* Launcher / Logo */ -.launcher { - padding: 0.4rem; - border-radius: 6px; -} - -.launcher:hover { - background: color-mix(in srgb, var(--bar-primary) 20%, transparent); -} - -/* Mobile menu toggle button */ -.mobile-menu-toggle { - display: none; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - padding: 0; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - cursor: pointer; - transition: all 0.15s ease; -} - -.mobile-menu-toggle:hover { - background: color-mix(in srgb, var(--bar-primary) 20%, transparent); - border-color: var(--bar-primary); -} - -@media (max-width: 768px) { - .mobile-menu-toggle { - display: flex; - } -} - -/* Workspaces */ -.workspaces { - gap: 0.15rem; - background: transparent; - padding: 0; -} - -.workspace { - display: flex; - align-items: center; - gap: 0.20rem; - padding: 0.35rem 0.4rem; - color: var(--bar-muted); - text-decoration: none; - border-radius: 4px; - transition: all 0.15s ease; - font-size: 0.75rem; -} - -.workspace:hover { - color: var(--bar-text); - background: var(--bar-bg-module); -} - -.workspace.active { - color: var(--bar-primary); - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); -} - -.workspace.external { - color: var(--bar-accent); -} - -.workspace.external:hover { - background: color-mix(in srgb, var(--bar-accent) 15%, transparent); -} - -.ws-name { - font-weight: 500; -} - -/* Window title */ -.window-title { - color: var(--bar-text); - font-size: 0.75rem; -} - -.title-icon { - color: var(--bar-primary); -} - -.title-text { - color: var(--bar-muted); -} - -/* Clock */ -.clock { - gap: 0.75rem; -} - -.time { - color: var(--bar-text); - font-weight: 600; -} - -.date { - color: var(--bar-muted); -} - -/* Theme selector */ -.theme-selector { - position: relative; - background: transparent; - padding: 0; -} - -.theme-trigger { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - cursor: pointer; - transition: all 0.15s ease; -} - -.theme-trigger:hover { - background: color-mix(in srgb, var(--bar-primary) 20%, transparent); - border-color: var(--bar-primary); -} - -.theme-dropdown { - position: absolute; - top: calc(100% + 0.5rem); - right: 0; - min-width: 200px; - background: var(--bar-bg); - border: 1px solid var(--bar-border); - border-radius: 8px; - padding: 0.5rem; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 1001; -} - -.dropdown-header { - padding: 0.25rem 0.5rem; - font-size: 0.65rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--bar-muted); -} - -.dropdown-divider { - height: 1px; - background: var(--bar-border); - margin: 0.5rem 0; -} - -.theme-option { - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; - padding: 0.5rem 0.75rem; - background: transparent; - border: none; - border-radius: 4px; - color: var(--bar-text); - cursor: pointer; - font-family: inherit; - font-size: 0.8rem; - text-align: left; - transition: all 0.15s ease; -} - -.theme-option:hover { - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); -} - -.theme-option.active { - color: var(--bar-primary); -} - -:global(.check) { - margin-left: auto; - color: var(--bar-primary); -} - -/* Backdrop */ -.backdrop { - position: fixed; - inset: 0; - background: transparent; - z-index: 997; - border: none; - cursor: default; -} - -/* Mobile menu dropdown */ -.mobile-menu { - position: fixed; - top: var(--navbar-height); - left: 0; - right: 0; - background: var(--bar-bg); - border-bottom: 1px solid var(--bar-border); - z-index: 1000; - padding: 1rem; - font-family: 'JetBrains Mono', 'Fira Code', monospace; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - pointer-events: auto; -} - -.mobile-nav-links { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.mobile-nav-link { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - color: var(--bar-text); - text-decoration: none; - border-radius: 6px; - transition: all 0.15s ease; - font-size: 0.9rem; - cursor: pointer; - position: relative; - z-index: 1; -} - -.mobile-nav-link:hover { - background: var(--bar-bg-module); - color: var(--bar-primary); -} - -.mobile-nav-link:active { - transform: scale(0.98); -} - -.mobile-nav-link.active { - color: var(--bar-primary); - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); -} - -.mobile-nav-link.external { - color: var(--bar-accent); -} - -.mobile-menu-divider { - height: 1px; - background: var(--bar-border); - margin: 1rem 0; -} - -.mobile-theme-section { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.mobile-section-header { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--bar-muted); - padding: 0 0.5rem; -} - -.mobile-theme-options { - display: flex; - gap: 0.5rem; -} - -.mobile-theme-btn { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.35rem; - padding: 0.75rem 0.5rem; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - cursor: pointer; - font-family: inherit; - font-size: 0.75rem; - transition: all 0.15s ease; - position: relative; - z-index: 1; -} - -.mobile-theme-btn:hover { - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); - border-color: var(--bar-primary); - color: var(--bar-primary); -} - -.mobile-theme-btn:active { - transform: scale(0.98); -} - -.mobile-theme-btn.active { - color: var(--bar-primary); - border-color: var(--bar-primary); - background: color-mix(in srgb, var(--bar-primary) 20%, transparent); -} - -.mobile-mode-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.75rem; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - cursor: pointer; - font-family: inherit; - font-size: 0.8rem; - transition: all 0.15s ease; - width: 100%; - position: relative; - z-index: 1; -} - -.mobile-mode-btn:hover { - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); - border-color: var(--bar-primary); - color: var(--bar-primary); -} - -.mobile-mode-btn:active { - transform: scale(0.98); -} - -/* Battery styles */ -.module.battery { - background: transparent; -} - -.battery-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.5rem; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - cursor: default; - font-family: inherit; - font-size: 0.8rem; - transition: all 0.12s ease; -} - -.battery-btn:hover { - background: color-mix(in srgb, var(--bar-primary) 12%, transparent); - border-color: var(--bar-primary); -} - -.battery-level { - font-weight: 600; - color: var(--bar-text); -} - -.battery-alert { - color: var(--bar-warning); -} - -/* Low battery visuals */ -.battery-btn.low { - border-color: color-mix(in srgb, var(--bar-warning) 60%, var(--bar-border)); - /* animation: battery-pulse 1.6s infinite ease-in-out; */ - box-shadow: 0 0 0 0 rgba(0,0,0,0); -} - -@keyframes battery-pulse { - 0% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } - 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--bar-warning) 12%, transparent); } - 100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } -} - -/* Charging visuals (green) - overrides low state */ -.battery-btn.charging { - border-color: color-mix(in srgb, var(--bar-success) 60%, var(--bar-border)); - background: color-mix(in srgb, var(--bar-success) 8%, var(--bar-bg-module)); - animation: none; -} - -.battery-level.charging { - color: var(--bar-success); -} - -/* Discord status module */ -.module.discord-status { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.5rem; - background: var(--bar-bg-module); - border: 1px solid var(--bar-border); - border-radius: 6px; - color: var(--bar-text); - font-size: 0.85rem; -} - -.discord-status .status-dot { - width: 10px; - height: 10px; - border-radius: 50%; - display: inline-block; - background: var(--bar-muted); - box-shadow: 0 0 0 2px rgba(0,0,0,0.04) inset; -} - -.discord-status .status-dot.online { background: var(--bar-success); } -.discord-status .status-dot.idle { background: var(--bar-warning); } -.discord-status .status-dot.dnd { background: var(--bar-error); } -.discord-status .status-dot.offline { background: var(--bar-muted); opacity: 0.7; } - -.discord-status .status-text { - color: var(--bar-text); - font-weight: 600; - font-size: 0.85rem; -} - -/* Keep the status on a single line and truncate if too long */ -.module.discord-status { - min-width: 0; -} - -.discord-status .status-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - max-width: 9rem; /* desktop default */ - vertical-align: middle; -} - -@media (max-width: 768px) { - /* tighter max width on mobile to keep everything on one line */ - .discord-status .status-text { - max-width: 6rem; - } - .module.discord-status { - padding: 0.2rem 0.45rem; - } -} - -/* Discord status button */ -.discord-status-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0; - background: transparent; - border: none; - color: inherit; - font-family: inherit; - font-size: inherit; - cursor: pointer; - transition: opacity 0.15s ease; -} - -.discord-status-btn:hover { - opacity: 0.85; -} - -/* Discord Popover */ -.module.discord-status { - position: relative; -} - -.discord-popover { - position: absolute; - top: calc(100% + 0.5rem); - right: -0.5rem; - width: 320px; - background: var(--bar-bg); - border: 1px solid var(--bar-border); - border-radius: 12px; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); - z-index: 1002; - overflow: hidden; -} - -.popover-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - background: color-mix(in srgb, var(--bar-primary) 8%, var(--bar-bg)); -} - -.popover-avatar { - position: relative; - flex-shrink: 0; - color: var(--bar-muted); - width: 48px; - height: 48px; -} - -.popover-avatar .avatar-img { - width: 48px; - height: 48px; - border-radius: 50%; - object-fit: cover; -} - -.popover-status-dot { - position: absolute; - bottom: 0; - right: 0; - width: 14px; - height: 14px; - border-radius: 50%; - border: 3px solid color-mix(in srgb, var(--bar-primary) 8%, var(--bar-bg)); - background: var(--bar-muted); -} - -.popover-status-dot.online { background: var(--bar-success); } -.popover-status-dot.idle { background: var(--bar-warning); } -.popover-status-dot.dnd { background: var(--bar-error); } -.popover-status-dot.offline { background: var(--bar-muted); opacity: 0.7; } - -.popover-user-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.15rem; - min-width: 0; -} - -.popover-name-row { - display: flex; - align-items: center; - gap: 0.5rem; - min-width: 0; -} - -.popover-username { - font-weight: 700; - font-size: 1rem; - color: var(--bar-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.popover-name-row { - display: flex; - align-items: center; - gap: 0.5rem; - min-width: 0; -} - -.popover-username { - font-weight: 700; - font-size: 1rem; - color: var(--bar-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: calc(100% - 92px); -} - -.popover-clan-tag { - display: inline-flex; - align-items: center; - justify-content: center; - height: 20px; - padding: 0 0.35rem; - background: color-mix(in srgb, var(--bar-primary) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--bar-primary) 30%, transparent); - border-radius: 4px; - font-size: 0.75rem; - font-weight: 600; - color: var(--bar-primary); - text-transform: uppercase; - letter-spacing: 0.02em; - line-height: 1; - flex-shrink: 0; -} - -/* .popover-clan-badge { - width: 36px; - height: 20px; - object-fit: contain; - border-radius: 4px; - flex-shrink: 0; - background: transparent; - border: none; - box-shadow: none; -} */ - -.popover-clan-tag { - display: inline-flex; - align-items: center; - justify-content: center; - height: 22px; /* slightly taller to match larger text */ - padding: 0 0.6rem; - background: color-mix(in srgb, var(--bar-primary) 12%, transparent); - border-radius: 4px; - font-size: 0.9rem; /* a bit larger than the badge */ - font-weight: 700; - color: var(--bar-primary); - text-transform: uppercase; - letter-spacing: 0.04em; - line-height: 1; - flex-shrink: 0; - margin-left: 0.25rem; -} - -.popover-clan-badge { - width: 15px; /* slightly smaller than tag text */ - height: 15px; - object-fit: contain; - border-radius: 4px; - flex-shrink: 0; - background: transparent; - border: none; - box-shadow: none; - margin-right: 0.35rem; -} - -/* Activities section */ -.popover-activities { - padding: 0.75rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.activity-card { - background: var(--bar-bg-module); - border-radius: 8px; - padding: 0.75rem; -} - -.activity-type-label { - font-size: 0.65rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--bar-muted); - margin-bottom: 0.5rem; - font-weight: 600; -} - -.activity-content { - display: flex; - gap: 0.75rem; -} - -.activity-images { - position: relative; - flex-shrink: 0; - width: 60px; - height: 60px; -} - -.activity-large-image { - width: 60px; - height: 60px; - border-radius: 8px; - object-fit: cover; -} - -.activity-small-image { - position: absolute; - bottom: -4px; - right: -4px; - width: 24px; - height: 24px; - border-radius: 50%; - border: 3px solid var(--bar-bg-module); - object-fit: cover; -} - -.activity-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.2rem; - min-width: 0; - justify-content: center; -} - -.activity-name { - font-weight: 600; - font-size: 0.85rem; - color: var(--bar-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.activity-details { - font-size: 0.75rem; - color: var(--bar-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* No activity state */ -.popover-no-activity { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - padding: 1.5rem; - color: var(--bar-muted); - font-size: 0.8rem; -} - -/* Mobile adjustments for popover */ -@media (max-width: 768px) { - .discord-popover { - position: fixed; - top: var(--navbar-height); - right: 0.5rem; - left: 0.5rem; - width: auto; - } -} - diff --git a/src/lib/assets/css/navbar.css b/src/lib/assets/css/navbar.css deleted file mode 100644 index f1cb5b5..0000000 --- a/src/lib/assets/css/navbar.css +++ /dev/null @@ -1,332 +0,0 @@ -.navbar { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; - background: var(--nav-bg); - border-bottom: 1px solid var(--nav-border); - backdrop-filter: blur(10px); - font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; -} - -.nav-container { - max-width: 1400px; - margin: 0 auto; - padding: 0.75rem 1.5rem; - display: flex; - align-items: center; - justify-content: space-between; - gap: 2rem; -} - -.nav-brand { - display: flex; - align-items: center; - gap: 0.25rem; - font-size: 0.9rem; - font-weight: 600; -} - -.terminal-prompt { - display: flex; - align-items: center; - gap: 0; -} - -.terminal-prompt .user { - color: var(--nav-accent); -} - -.terminal-prompt .separator { - color: var(--nav-text); -} - -.terminal-prompt .path { - color: var(--nav-primary); -} - -.terminal-prompt .symbol { - color: var(--nav-text); - margin-left: 0.25rem; -} - -.cursor { - width: 0.5em; - height: 1.1em; - background: var(--nav-primary); - animation: blink 1s step-end infinite; - margin-left: 0.25rem; - vertical-align: baseline; - transform: translateY(0.15em); -} - -@keyframes blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -.mobile-toggle { - display: none; - background: none; - border: none; - padding: 0.5rem; - cursor: pointer; -} - -.hamburger { - display: block; - width: 24px; - height: 2px; - background: var(--nav-text); - position: relative; - transition: all 0.3s ease; -} - -.hamburger::before, -.hamburger::after { - content: ''; - position: absolute; - width: 24px; - height: 2px; - background: var(--nav-text); - transition: all 0.3s ease; -} - -.hamburger::before { - top: -7px; -} - -.hamburger::after { - top: 7px; -} - -.hamburger.open { - background: transparent; -} - -.hamburger.open::before { - top: 0; - transform: rotate(45deg); -} - -.hamburger.open::after { - top: 0; - transform: rotate(-45deg); -} - -.nav-links { - display: flex; - align-items: center; - gap: 1.5rem; - flex: 1; -} - -.nav-link { - color: var(--nav-text); - text-decoration: none; - font-size: 0.85rem; - padding: 0.5rem 0.75rem; - border-radius: 4px; - transition: all 0.2s ease; - display: flex; - align-items: center; - position: relative; - overflow: hidden; -} - -.nav-link::before { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 0; - height: 2px; - background: var(--nav-primary); - transition: width 0.3s ease; -} - -.nav-link:hover::before { - width: 100%; -} - -.nav-link:hover { - color: var(--nav-primary); -} - -.link-prefix { - color: var(--nav-muted); - margin-right: 2px; -} - -.nav-controls { - display: flex; - align-items: center; - gap: 1rem; -} - -.theme-selector { - position: relative; -} - -.theme-button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background: transparent; - border: 1px solid var(--nav-border); - border-radius: 6px; - color: var(--nav-text); - cursor: pointer; - font-family: inherit; - font-size: 0.8rem; - transition: all 0.2s ease; -} - -.theme-button:hover { - border-color: var(--nav-primary); - background: color-mix(in srgb, var(--nav-primary) 10%, transparent); -} - -.theme-icon { - font-size: 1rem; - display: flex; - align-items: center; -} - -.theme-label { - text-transform: capitalize; -} - -:global(.dropdown-arrow) { - transition: transform 0.2s ease; -} - -:global(.dropdown-arrow.open) { - transform: rotate(180deg); -} - -.theme-dropdown { - position: absolute; - top: calc(100% + 0.5rem); - right: 0; - min-width: 180px; - background: var(--nav-bg); - border: 1px solid var(--nav-border); - border-radius: 8px; - padding: 0.5rem; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); - z-index: 1001; -} - -.theme-option { - display: flex; - align-items: center; - gap: 0.75rem; - width: 100%; - padding: 0.625rem 0.75rem; - background: transparent; - border: none; - border-radius: 6px; - color: var(--nav-text); - cursor: pointer; - font-family: inherit; - font-size: 0.85rem; - text-align: left; - transition: all 0.2s ease; -} - -.theme-option:hover { - background: color-mix(in srgb, var(--nav-primary) 15%, transparent); -} - -.theme-option.active { - background: color-mix(in srgb, var(--nav-primary) 20%, transparent); - color: var(--nav-primary); -} - -.option-icon { - font-size: 1.1rem; - display: flex; - align-items: center; -} - -.option-label { - flex: 1; -} - -:global(.check-mark) { - color: var(--nav-primary); -} - -.mode-toggle { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: transparent; - border: 1px solid var(--nav-border); - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease; - color: var(--nav-text); -} - -.mode-toggle:hover { - border-color: var(--nav-primary); - background: color-mix(in srgb, var(--nav-primary) 10%, transparent); - color: var(--nav-primary); -} - -.backdrop { - position: fixed; - inset: 0; - background: transparent; - z-index: 999; - border: none; - cursor: default; -} - -@media (max-width: 768px) { - .nav-container { - flex-wrap: wrap; - padding: 0.75rem 1rem; - } - - .mobile-toggle { - display: block; - order: 2; - } - - .nav-links { - order: 4; - width: 100%; - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease, padding 0.3s ease; - } - - .nav-links.expanded { - max-height: 200px; - padding-top: 1rem; - } - - .nav-controls { - order: 3; - margin-left: auto; - margin-right: 1rem; - } - - .theme-label { - display: none; - } - - .theme-button { - padding: 0.5rem; - } -} diff --git a/src/lib/assets/css/terminal-page.css b/src/lib/assets/css/terminal-page.css deleted file mode 100644 index 81b253e..0000000 --- a/src/lib/assets/css/terminal-page.css +++ /dev/null @@ -1,224 +0,0 @@ -.terminal-page { - font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; - background: var(--terminal-bg); - border: 1px solid var(--terminal-border); - border-radius: 12px; - overflow: hidden; - box-shadow: - 0 25px 50px -12px rgba(0, 0, 0, 0.4), - 0 0 0 1px rgba(255, 255, 255, 0.05) inset; - animation: terminalFadeIn 0.5s ease-out; - display: flex; - flex-direction: column; - height: 100%; -} - -@keyframes terminalFadeIn { - from { - opacity: 0; - transform: translateY(20px) scale(0.98); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.terminal-header { - display: flex; - align-items: center; - padding: 0.75rem 1rem; - background: var(--terminal-bg-light); - border-bottom: 1px solid var(--terminal-border); - flex-shrink: 0; -} - -.terminal-buttons { - display: flex; - gap: 8px; -} - -.terminal-buttons .btn { - width: 12px; - height: 12px; - border-radius: 50%; - cursor: pointer; - transition: opacity 0.2s; -} - -.terminal-buttons .btn:hover { - opacity: 0.8; -} - -.terminal-buttons .close { - background: #ff5f56; -} - -.terminal-buttons .minimize { - background: #ffbd2e; -} - -.terminal-buttons .maximize { - background: #27ca40; -} - -.terminal-title { - flex: 1; - text-align: center; - font-size: 0.8rem; - color: var(--terminal-muted); - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; -} - -.terminal-icon { - font-size: 1rem; -} - -.terminal-spacer { - width: 52px; -} - -.terminal-body { - padding: 1rem 1.25rem; - flex: 1; - overflow-y: auto; - font-size: 0.9rem; - line-height: 1.6; -} - -.terminal-line { - display: flex; - align-items: flex-start; - flex-wrap: wrap; - margin-bottom: 0.25rem; - animation: lineSlideIn 0.2s ease-out; - min-height: 1.6em; -} - -.terminal-line.blank { - min-height: 0.8em; -} - -@keyframes lineSlideIn { - from { - opacity: 0; - transform: translateX(-10px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.prompt { - display: inline-flex; - margin-right: 0.5rem; - flex-shrink: 0; -} - -.prompt .user { - color: var(--terminal-user); -} - -.prompt .separator { - color: var(--terminal-text); -} - -.prompt .path { - color: var(--terminal-path); -} - -.prompt .symbol { - color: var(--terminal-text); - margin-left: 0.25rem; -} - -.content { - color: var(--terminal-text); - word-break: break-word; - white-space: pre-wrap; -} - -.terminal-line.output .content { - color: var(--terminal-muted); -} - -.terminal-line.error .content { - color: #f38ba8; -} - -.terminal-line.success .content { - color: #a6e3a1; -} - -.terminal-line.info .content { - color: var(--terminal-primary); -} - -.terminal-line.header .content { - color: var(--terminal-accent); - font-weight: 600; - font-size: 1rem; -} - -.header-text { - color: var(--terminal-accent); - font-weight: 600; -} - -.terminal-image { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.5rem 0; -} - -.terminal-image img { - border-radius: 8px; - border: 1px solid var(--terminal-border); - background: var(--terminal-bg-light); - object-fit: contain; -} - -.image-caption { - color: var(--terminal-muted); - font-size: 0.8rem; - font-style: italic; -} - -.cursor { - display: inline-block; - width: 0.5em; - height: 1.1em; - background: var(--terminal-primary); - animation: cursorBlink 1s step-end infinite; - margin-left: 2px; - vertical-align: baseline; - transform: translateY(0.15em); -} - -@keyframes cursorBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -/* Scrollbar styling */ -.terminal-body::-webkit-scrollbar { - width: 8px; -} - -.terminal-body::-webkit-scrollbar-track { - background: transparent; -} - -.terminal-body::-webkit-scrollbar-thumb { - background: var(--terminal-border); - border-radius: 4px; -} - -.terminal-body::-webkit-scrollbar-thumb:hover { - background: var(--terminal-muted); -} diff --git a/src/lib/assets/css/terminal-tui.css b/src/lib/assets/css/terminal-tui.css deleted file mode 100644 index 1ff69dd..0000000 --- a/src/lib/assets/css/terminal-tui.css +++ /dev/null @@ -1,61 +0,0 @@ -.tui-terminal { - font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; - background: var(--terminal-bg); - border: 2px solid var(--terminal-border); - border-radius: 0; - overflow: hidden; - position: relative; - display: flex; - flex-direction: column; - width: 100%; - margin: 0 auto; - height: calc(100vh - var(--navbar-height)); - max-height: calc(100vh - var(--navbar-height)); -} - -.tui-terminal:focus-within .tui-border-glow { - opacity: 1; -} - -/* Hyprland-style animated border glow */ -.tui-border-glow { - position: absolute; - inset: -2px; - border-radius: 0; - background: linear-gradient( - 45deg, - var(--terminal-primary), - var(--terminal-accent), - var(--terminal-primary), - var(--terminal-accent) - ); - background-size: 400% 400%; - animation: borderGlow 8s ease infinite; - opacity: 0.5; - z-index: -1; - filter: blur(4px); - transition: opacity 0.3s ease; -} - -@keyframes borderGlow { - 0%, 100% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } -} - -.tui-content { - display: flex; - flex-direction: column; - height: 100%; - background: var(--terminal-bg); - position: relative; - z-index: 1; -} - -/* Responsive */ -@media (max-width: 768px) { - .tui-terminal { - width: 100%; - height: calc(100vh - var(--navbar-height)); - max-height: calc(100vh - var(--navbar-height)); - } -} diff --git a/src/lib/assets/css/terminal.css b/src/lib/assets/css/terminal.css deleted file mode 100644 index 54c78e6..0000000 --- a/src/lib/assets/css/terminal.css +++ /dev/null @@ -1,184 +0,0 @@ -.terminal { - font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; - background: var(--terminal-bg); - border: 1px solid var(--terminal-border); - border-radius: 12px; - overflow: hidden; - box-shadow: - 0 25px 50px -12px rgba(0, 0, 0, 0.4), - 0 0 0 1px rgba(255, 255, 255, 0.05) inset; - animation: terminalFadeIn 0.5s ease-out; -} - -@keyframes terminalFadeIn { - from { - opacity: 0; - transform: translateY(20px) scale(0.98); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.terminal-header { - display: flex; - align-items: center; - padding: 0.75rem 1rem; - background: color-mix(in srgb, var(--terminal-bg) 80%, black); - border-bottom: 1px solid var(--terminal-border); -} - -.terminal-buttons { - display: flex; - gap: 8px; -} - -.terminal-buttons .btn { - width: 12px; - height: 12px; - border-radius: 50%; - cursor: pointer; - transition: opacity 0.2s; -} - -.terminal-buttons .btn:hover { - opacity: 0.8; -} - -.terminal-buttons .close { - background: #ff5f56; -} - -.terminal-buttons .minimize { - background: #ffbd2e; -} - -.terminal-buttons .maximize { - background: #27ca40; -} - -.terminal-title { - flex: 1; - text-align: center; - font-size: 0.8rem; - color: var(--terminal-muted); - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; -} - -.terminal-icon { - font-size: 1rem; -} - -.terminal-spacer { - width: 52px; -} - -.terminal-body { - padding: 1rem 1.25rem; - min-height: 200px; - max-height: 400px; - overflow-y: auto; - font-size: 0.9rem; - line-height: 1.6; -} - -.terminal-line { - display: flex; - align-items: center; - flex-wrap: wrap; - margin-bottom: 0.25rem; - animation: lineSlideIn 0.2s ease-out; -} - -@keyframes lineSlideIn { - from { - opacity: 0; - transform: translateX(-10px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.prompt { - display: inline-flex; - margin-right: 0.5rem; - flex-shrink: 0; -} - -.prompt .user { - color: var(--terminal-user); -} - -.prompt .separator { - color: var(--terminal-text); -} - -.prompt .path { - color: var(--terminal-path); -} - -.prompt .symbol { - color: var(--terminal-text); - margin-left: 0.25rem; -} - -.content { - color: var(--terminal-text); - word-break: break-word; -} - -.terminal-line.output .content { - color: var(--terminal-muted); -} - -.terminal-line.error .content { - color: #ff6b6b; -} - -.terminal-line.success .content { - color: #51cf66; -} - -.terminal-line.info .content { - color: var(--terminal-primary); -} - -.cursor { - display: inline-block; - width: 0.5em; - height: 1.1em; - background: var(--terminal-primary); - animation: cursorBlink 1s step-end infinite; - margin-left: 2px; - vertical-align: baseline; - transform: translateY(0.15em); -} - -@keyframes cursorBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -/* Scrollbar styling */ -.terminal-body::-webkit-scrollbar { - width: 8px; -} - -.terminal-body::-webkit-scrollbar-track { - background: transparent; -} - -.terminal-body::-webkit-scrollbar-thumb { - background: var(--terminal-border); - border-radius: 4px; -} - -.terminal-body::-webkit-scrollbar-thumb:hover { - background: var(--terminal-muted); -} diff --git a/src/lib/assets/css/tui-accordion.css b/src/lib/assets/css/tui-accordion.css deleted file mode 100644 index 8e9e991..0000000 --- a/src/lib/assets/css/tui-accordion.css +++ /dev/null @@ -1,78 +0,0 @@ -.tui-accordion { - margin: 0.5rem 0; - border: 1px solid var(--terminal-border); - border-radius: 6px; - overflow: hidden; -} - -.tui-accordion.inline { - display: inline-block; - width: auto; - min-width: 200px; - vertical-align: top; - margin: 0 0.5rem 0 0; -} - -.accordion-item { - border-bottom: 1px solid var(--terminal-border); -} - -.accordion-item:last-child { - border-bottom: none; -} - -.accordion-header { - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; - padding: 0.6rem 0.75rem; - background: transparent; - border: none; - color: var(--terminal-text); - font-family: inherit; - font-size: 0.85rem; - font-weight: 500; - text-align: left; - cursor: pointer; - transition: background 0.15s ease; -} - -.accordion-header:hover { - background: rgba(255, 255, 255, 0.03); -} - -.accordion-item.open .accordion-header { - background: rgba(0, 0, 0, 0.1); - border-bottom: 1px solid var(--terminal-border); -} - -:global(.accordion-icon) { - color: var(--accordion-accent); - transition: transform 0.2s ease; -} - -.accordion-title { - flex: 1; -} - -.accordion-content { - padding: 0.75rem; - color: var(--terminal-muted); - font-size: 0.85rem; - line-height: 1.6; - white-space: pre-wrap; - animation: slideDown 0.2s ease-out; -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-5px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-body.css b/src/lib/assets/css/tui-body.css deleted file mode 100644 index a61df26..0000000 --- a/src/lib/assets/css/tui-body.css +++ /dev/null @@ -1,307 +0,0 @@ -.tui-body { - flex: 1; - padding: 1rem 1.25rem 2rem 1.25rem; - overflow-y: auto; - overflow-x: hidden; - font-size: 0.9rem; - line-height: 1.7; - min-height: 0; -} - -/* Inline group wrapper - groups consecutive inline elements */ -.tui-inline-group { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 0.5rem; - margin-bottom: 0.2rem; - animation: lineSlideIn 0.15s ease-out; -} - -/* When inline group contains an image, allow text to wrap beside it */ -.tui-inline-group:has(.inline-image) { - flex-wrap: nowrap; - align-items: flex-start; -} - -.tui-inline-group:has(.inline-image) .inline-content, -.tui-inline-group:has(.inline-image) .tui-group { - flex: 1; - min-width: 0; -} - -.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; - align-items: flex-start; - flex-wrap: wrap; - margin-bottom: 0.2rem; - animation: lineSlideIn 0.15s ease-out; - min-height: 1.7em; -} - -.tui-line.blank { - min-height: 0.15em; -} - -@keyframes lineSlideIn { - from { - opacity: 0; - transform: translateX(-5px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -/* Prompt styling */ -.prompt { - display: inline-flex; - margin-right: 0.5rem; - flex-shrink: 0; -} - -.prompt .user { - color: var(--terminal-user); -} - -.prompt .at { - color: var(--terminal-muted); -} - -.prompt .host { - color: var(--terminal-accent); -} - -.prompt .separator { - color: var(--terminal-muted); -} - -.prompt .path { - color: var(--terminal-path); -} - -.prompt .symbol { - color: var(--terminal-muted); - margin-left: 0.25rem; -} - -/* Content */ -.content { - color: var(--terminal-text); - word-break: break-word; - white-space: pre-wrap; -} - -.tui-line.output .content { - color: var(--terminal-muted); -} - -.tui-line.error .content { - color: #f38ba8; -} - -.tui-line.success .content { - color: #a6e3a1; -} - -.tui-line.info .content { - color: var(--terminal-primary); -} - -.tui-line.warning .content { - color: #f9e2af; -} - -.header-text { - color: var(--terminal-accent); - font-weight: 700; - font-size: 1.35rem; - display: flex; - align-items: center; - gap: 0.5rem; - line-height: 1.2; -} - -.header-text.inline-header { - display: inline-flex; -} - -:global(.header-icon) { - opacity: 0.7; -} - -:global(.inline-icon) { - display: inline-block; - vertical-align: middle; - margin: 0 0.15em; -} - -/* Divider */ -.tui-divider { - display: flex; - align-items: center; - gap: 1rem; - margin: 0.75rem 0; -} - -.divider-line { - flex: 1; - height: 1px; - background: linear-gradient(to right, - transparent, - var(--terminal-border), - transparent); -} - -.divider-text { - color: var(--terminal-muted); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.1em; -} - -/* Images */ -.tui-image { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.5rem 0; -} - -/* Inline images */ -.tui-image.inline-image { - display: inline-flex; - padding: 0; - vertical-align: middle; - flex-shrink: 0; -} - -.tui-image img { - border-radius: 6px; - border: 1px solid var(--terminal-border); - background: var(--terminal-bg-light); - object-fit: contain; -} - -.image-caption { - color: var(--terminal-muted); - font-size: 0.8rem; - font-style: italic; -} - -/* Cursor */ -.cursor { - display: inline-block; - width: 0.5em; - height: 1.1em; - background: var(--terminal-primary); - background: var(--terminal-primary); - margin-left: 2px; - vertical-align: baseline; - transform: translateY(0.15em); -} - -.cursor.blink { - animation: cursorBlink 1s step-end infinite; -} - -@keyframes cursorBlink { - - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0; - } -} - -/* Scrollbar */ -.tui-body::-webkit-scrollbar { - width: 6px; -} - -.tui-body::-webkit-scrollbar-track { - background: transparent; -} - -.tui-body::-webkit-scrollbar-thumb { - background: var(--terminal-border); - border-radius: 3px; -} - -.tui-body::-webkit-scrollbar-thumb:hover { - background: var(--terminal-muted); -} - -/* Mobile: stack inline groups vertically */ -@media (max-width: 768px) { - .tui-inline-group { - flex-direction: column; - flex-wrap: nowrap; - align-items: stretch; - } - - .tui-inline-group:has(.inline-image) { - flex-wrap: nowrap; - flex-direction: column; - } - - /* Keep inline flow for specific containers */ - .tui-card .tui-inline-group, - .tui-group .tui-inline-group, - .tui-accordion .tui-inline-group { - flex-direction: row; - flex-wrap: wrap; - align-items: flex-start; - } - - .tui-inline-group .inline-image img { - max-width: 100% !important; - width: 100%; - } - - /* TuiGroup inline on mobile should stack */ - .tui-group.inline { - flex-direction: column; - align-items: stretch; - } - - .tui-group.inline .inline-image img { - max-width: 100% !important; - width: 100%; - } - - .mobile-hidden { - display: none !important; - } -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-button.css b/src/lib/assets/css/tui-button.css deleted file mode 100644 index d47b3c3..0000000 --- a/src/lib/assets/css/tui-button.css +++ /dev/null @@ -1,77 +0,0 @@ -.tui-button { - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; - padding: 0.5rem 0.75rem; - margin: 0.75rem 0; - background: transparent; - border: 1px solid var(--btn-color); - border-radius: 4px; - color: var(--btn-color); - font-family: inherit; - font-size: 0.9rem; - text-align: left; - cursor: pointer; - transition: all 0.15s ease; -} - -.tui-button.no-border { - border-color: transparent; -} - -.tui-button.flex-expand { - flex: 1; - width: auto; -} - -/* Inline button styles */ -.tui-button.inline { - width: auto; - display: inline-flex; - margin: 0; - padding: 0.35rem 0.6rem; - border: 1px solid color-mix(in srgb, var(--btn-color) 40%, transparent); -} - -.tui-button.inline .btn-indicator { - display: none; -} - -.tui-button:hover, -.tui-button.selected { - background: color-mix(in srgb, var(--btn-color) 15%, transparent); - border-color: var(--btn-color); -} - -.btn-indicator { - color: var(--btn-color); - font-size: 0.8rem; - width: 1rem; -} - -.btn-text { - flex: 1; -} - -:global(.btn-arrow) { - opacity: 0.5; -} - -.tui-button.selected :global(.btn-arrow) { - opacity: 1; -} - -:global(.inline-icon) { - display: inline-block; - vertical-align: middle; - margin: 0 0.15em; -} - -/* Mobile: inline buttons become full-width */ -@media (max-width: 768px) { - .tui-button.inline { - width: 100%; - display: flex; - } -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-card-grid.css b/src/lib/assets/css/tui-card-grid.css deleted file mode 100644 index db6b153..0000000 --- a/src/lib/assets/css/tui-card-grid.css +++ /dev/null @@ -1,232 +0,0 @@ -.tui-card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; - padding: 0.5rem 0; - width: 100%; -} - -.tui-card-grid.inline { - display: inline-flex; - flex-wrap: wrap; - width: auto; - gap: 0.5rem; - vertical-align: top; - padding: 0; - margin: 0 0.5rem 0 0; -} - -.tui-card { - background: var(--terminal-bg); - border: 1px solid var(--terminal-border); - border-radius: 8px; - overflow: hidden; - display: flex; - flex-direction: column; - transition: all 0.2s ease; - font-family: 'JetBrains Mono', monospace; -} - -.tui-card:hover { - border-color: var(--terminal-primary); - transform: translateY(-2px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); -} - -.tui-card.featured { - border-color: var(--terminal-accent); -} - -.card-image { - position: relative; - width: 100%; - height: 140px; - overflow: hidden; - background: var(--terminal-bg-light); -} - -.card-image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.tui-card:hover .card-image img { - transform: scale(1.05); -} - -.card-header-badge { - padding: 0.5rem 0.75rem 0; -} - -.featured-badge { - position: absolute; - top: 0.5rem; - right: 0.5rem; - background: rgba(0, 0, 0, 0.75); - color: var(--terminal-accent); - padding: 0.15rem 0.4rem; - border-radius: 3px; - font-size: 0.65rem; - font-weight: 600; - backdrop-filter: blur(4px); -} - -.card-header-badge .featured-badge { - position: static; - display: inline-block; -} - -.card-body { - padding: 0.75rem; - display: flex; - flex-direction: column; - gap: 0.4rem; - flex: 1; -} - -.card-title { - margin: 0; - font-size: 0.9rem; - font-weight: 600; - color: var(--terminal-text); - line-height: 1.3; -} - -.card-meta { - display: flex; - align-items: center; - gap: 0.3rem; - font-size: 0.7rem; - color: var(--terminal-primary); -} - -.meta-icon { - font-size: 0.75rem; -} - -.hackathon-name { - font-weight: 500; -} - -.year { - color: var(--terminal-muted); -} - -.card-location { - display: flex; - align-items: center; - gap: 0.3rem; - font-size: 0.65rem; - color: var(--terminal-muted); -} - -.awards { - display: flex; - flex-direction: column; - gap: 0.2rem; - margin: 0.25rem 0; -} - -.award { - display: flex; - align-items: center; - gap: 0.3rem; - font-size: 0.7rem; -} - -.award-icon { - font-size: 0.75rem; -} - -.award-text { - color: #a6e3a1; - font-weight: 500; -} - -.card-desc { - margin: 0; - font-size: 0.7rem; - color: var(--terminal-muted); - line-height: 1.4; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.tags { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - margin-top: auto; -} - -.tag { - background: color-mix(in srgb, var(--terminal-primary) 15%, transparent); - color: var(--terminal-primary); - padding: 0.1rem 0.35rem; - border-radius: 3px; - font-size: 0.55rem; - font-weight: 500; - text-transform: lowercase; -} - -.tui-card-grid .warning { - font-size: 0.6rem; - color: #f9e2af; - padding: 0.2rem 0.35rem; - background: rgba(249, 226, 175, 0.1); - border-radius: 3px; - width: fit-content; -} - -.card-actions { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - margin-top: 0.5rem; - padding-top: 0.5rem; - border-top: 1px solid var(--terminal-border); -} - -.action-btn { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.5rem; - background: transparent; - border: 1px solid var(--terminal-border); - border-radius: 4px; - color: var(--terminal-text); - font-size: 0.65rem; - font-weight: 500; - text-decoration: none; - transition: all 0.15s ease; - font-family: inherit; -} - -.action-btn:hover { - background: var(--terminal-primary); - border-color: var(--terminal-primary); - color: var(--terminal-bg); -} - -.action-btn.primary { - background: var(--terminal-primary); - border-color: var(--terminal-primary); - color: var(--terminal-bg); -} - -.action-btn.primary:hover { - background: var(--terminal-accent); - border-color: var(--terminal-accent); -} - -@media (max-width: 600px) { - .tui-card-grid { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-card.css b/src/lib/assets/css/tui-card.css deleted file mode 100644 index 72fb945..0000000 --- a/src/lib/assets/css/tui-card.css +++ /dev/null @@ -1,46 +0,0 @@ -.tui-card { - border: 1px solid var(--terminal-border); - border-left: 3px solid var(--card-color); - background: color-mix(in srgb, var(--terminal-bg) 50%, var(--terminal-bg-light)); - border-radius: 4px; - padding: 0; - margin: 0.5rem 0; - width: 100%; -} - -.tui-card.inline { - display: inline-block; - width: auto; - margin: 0 0.5rem 0 0; - vertical-align: top; -} - -.card-title { - color: var(--card-color); - font-weight: bold; - margin-bottom: 0.5rem; - font-size: 1rem; -} - -.card-content { - color: var(--terminal-text); - font-size: 0.9rem; - line-height: 1.5; -} - -.card-footer { - margin-top: 0.75rem; - padding-top: 0.5rem; - border-top: 1px dashed var(--terminal-border); - color: var(--terminal-muted); - font-size: 0.8rem; -} - -/* Mobile: inline cards become full-width */ -@media (max-width: 768px) { - .tui-card.inline { - display: block; - width: 100%; - margin: 0.5rem 0; - } -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-checkbox.css b/src/lib/assets/css/tui-checkbox.css deleted file mode 100644 index 679c400..0000000 --- a/src/lib/assets/css/tui-checkbox.css +++ /dev/null @@ -1,55 +0,0 @@ -.tui-checkbox { - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0.35rem 0; - padding: 0.35rem 0.5rem; - font-size: 0.9rem; - cursor: pointer; - border-radius: 4px; - transition: all 0.15s ease; - user-select: none; -} - -.tui-checkbox.inline { - display: inline-flex; - margin: 0 0.75rem 0 0; -} - -.tui-checkbox:hover:not(.disabled) { - background: color-mix(in srgb, var(--checkbox-color) 10%, transparent); -} - -.tui-checkbox:focus-visible { - outline: 1px solid var(--checkbox-color); - outline-offset: 2px; -} - -.tui-checkbox.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.checkbox-box { - font-family: inherit; - font-weight: bold; - color: var(--terminal-muted); - transition: color 0.15s ease; -} - -.tui-checkbox.checked .checkbox-box, -.checkbox-box.indeterminate { - color: var(--checkbox-color); -} - -:global(.checkbox-icon) { - color: var(--checkbox-color); -} - -.checkbox-label { - color: var(--terminal-text); -} - -.tui-checkbox.disabled .checkbox-label { - color: var(--terminal-muted); -} diff --git a/src/lib/assets/css/tui-footer.css b/src/lib/assets/css/tui-footer.css deleted file mode 100644 index 3e76510..0000000 --- a/src/lib/assets/css/tui-footer.css +++ /dev/null @@ -1,59 +0,0 @@ -.tui-statusbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.4rem 0.75rem; - font-size: 0.75rem; - background: var(--terminal-bg-light); - border-color: var(--terminal-border); -} - -.tui-statusbar.bottom { - border-top: 1px solid var(--terminal-border); -} - -.status-left, .status-right { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--terminal-muted); -} - -.status-center { - color: var(--terminal-primary); - font-weight: 600; -} - -.skip-btn { - display: flex; - align-items: center; - gap: 0.3rem; - padding: 0.15rem 0.5rem; - background: var(--terminal-border); - border: 1px solid transparent; - border-radius: 3px; - color: var(--terminal-muted); - font-family: inherit; - font-size: 0.7rem; - cursor: pointer; - transition: all 0.15s ease; -} - -.skip-btn:hover { - background: var(--terminal-primary); - color: var(--terminal-bg); - border-color: var(--terminal-primary); -} - -.typing-indicator { - animation: pulse 1s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.line-count { - color: var(--terminal-muted); -} diff --git a/src/lib/assets/css/tui-header.css b/src/lib/assets/css/tui-header.css deleted file mode 100644 index 88b3d3a..0000000 --- a/src/lib/assets/css/tui-header.css +++ /dev/null @@ -1,71 +0,0 @@ -.tui-statusbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.4rem 0.75rem; - font-size: 0.75rem; - background: var(--terminal-bg-light); - border-color: var(--terminal-border); - gap: 1rem; - position: relative; -} - -.tui-statusbar.top { - border-bottom: 1px solid var(--terminal-border); -} - -.status-left { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--terminal-muted); - flex-shrink: 0; - z-index: 1; -} - -.status-right { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--terminal-muted); - flex-shrink: 0; - z-index: 1; -} - -.status-center { - position: absolute; - left: 50%; - transform: translateX(-50%); - color: var(--terminal-primary); - font-weight: 600; - text-align: center; - white-space: nowrap; -} - -.hint { - padding: 0.1rem 0.4rem; - background: var(--terminal-border); - border-radius: 3px; - font-size: 0.7rem; -} - -.user-host { - display: inline; -} - -@media (max-width: 768px) { - .hint { - display: none; - } - - .user-host { - display: none; - } - - .status-center { - position: static; - transform: none; - left: 0; - } -} - diff --git a/src/lib/assets/css/tui-input.css b/src/lib/assets/css/tui-input.css deleted file mode 100644 index 24dac23..0000000 --- a/src/lib/assets/css/tui-input.css +++ /dev/null @@ -1,96 +0,0 @@ -.tui-input { - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.tui-input.inline { - display: inline-flex; - flex-direction: column; - margin: 0 0.5rem 0 0; -} - -.input-label { - display: flex; - align-items: center; - gap: 0.35rem; - margin-bottom: 0.35rem; - color: var(--terminal-muted); - font-size: 0.85rem; -} - -:global(.label-icon) { - color: var(--input-color); -} - -.input-wrapper { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.5rem 0.75rem; - background: color-mix(in srgb, var(--terminal-bg) 80%, black); - border: 1px solid var(--terminal-muted); - border-radius: 4px; - transition: all 0.15s ease; -} - -.input-wrapper:focus-within { - border-color: var(--input-color); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent); -} - -.tui-input.error .input-wrapper { - border-color: #f38ba8; -} - -.tui-input.disabled .input-wrapper { - opacity: 0.5; - cursor: not-allowed; -} - -.input-prompt { - color: var(--input-color); - font-weight: bold; - user-select: none; -} - -.input-affix { - color: var(--terminal-muted); - font-size: 0.85rem; -} - -.input-affix.prefix { - margin-right: 0.25rem; -} - -.input-affix.suffix { - margin-left: 0.25rem; -} - -input { - flex: 1; - min-width: 0; - background: transparent; - border: none; - color: var(--terminal-text); - font-family: inherit; - font-size: inherit; - outline: none; -} - -input::placeholder { - color: var(--terminal-muted); - opacity: 0.6; -} - -input:disabled { - cursor: not-allowed; -} - -.input-error { - display: flex; - align-items: center; - gap: 0.35rem; - margin-top: 0.35rem; - color: #f38ba8; - font-size: 0.8rem; -} diff --git a/src/lib/assets/css/tui-link.css b/src/lib/assets/css/tui-link.css deleted file mode 100644 index 5da3408..0000000 --- a/src/lib/assets/css/tui-link.css +++ /dev/null @@ -1,45 +0,0 @@ -.tui-link { - display: inline-flex; - align-items: center; - gap: 0.25rem; -} - -:global(.link-icon) { - color: var(--link-color); - opacity: 0.8; -} - -.link-text { - background: none; - border: none; - padding: 0; - margin: 0; - font-family: inherit; - font-size: inherit; - color: var(--link-color); - text-decoration: underline; - text-decoration-style: dotted; - text-underline-offset: 2px; - cursor: pointer; - transition: all 0.15s ease; -} - -.link-text:hover { - text-decoration-style: solid; - filter: brightness(1.2); -} - -:global(.link-external) { - color: var(--link-color); - opacity: 0.5; -} - -.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/assets/css/tui-progress.css b/src/lib/assets/css/tui-progress.css deleted file mode 100644 index 885f340..0000000 --- a/src/lib/assets/css/tui-progress.css +++ /dev/null @@ -1,104 +0,0 @@ -.tui-progress { - margin: 0.5rem 0; - font-size: 0.85rem; -} - -/* Inline progress styles */ -.tui-progress.inline { - display: inline-flex; - align-items: center; - gap: 0.5rem; - margin: 0; - min-width: 120px; -} - -.tui-progress.inline .progress-label { - margin-bottom: 0; - white-space: nowrap; -} - -.tui-progress.inline .progress-bar { - flex: 1; - min-width: 80px; - height: 0.8rem; -} - -.tui-progress.inline .progress-value { - margin-top: 0; - white-space: nowrap; -} - -.progress-label { - color: var(--terminal-text); - margin-bottom: 0.25rem; -} - -.progress-bar { - position: relative; - height: 1.2rem; - background: var(--terminal-border); - border-radius: 3px; - overflow: hidden; -} - -.progress-fill { - position: absolute; - top: 0; - left: 0; - height: 100%; - background: linear-gradient(90deg, var(--progress-color), color-mix(in srgb, var(--progress-color) 80%, white 20%)); - border-radius: 3px; - transition: width 0.3s ease; -} - -.progress-glow { - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 20px; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3)); - animation: shimmer 1.5s infinite; -} - -@keyframes shimmer { - 0% { opacity: 0; } - 50% { opacity: 1; } - 100% { opacity: 0; } -} - -.progress-blocks { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - gap: 2px; - padding: 3px; -} - -.block { - flex: 1; - background: rgba(0, 0, 0, 0.2); - border-radius: 1px; - transition: background 0.2s; -} - -.block.filled { - background: transparent; -} - -.progress-value { - text-align: right; - color: var(--progress-color); - font-size: 0.75rem; - margin-top: 0.25rem; - font-weight: 600; -} - -:global(.inline-icon) { - display: inline-block; - vertical-align: middle; - margin: 0 0.15em; -} diff --git a/src/lib/assets/css/tui-radio.css b/src/lib/assets/css/tui-radio.css deleted file mode 100644 index 3a879ab..0000000 --- a/src/lib/assets/css/tui-radio.css +++ /dev/null @@ -1,87 +0,0 @@ -.tui-radio-group { - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.tui-radio-group.inline { - display: inline-flex; - flex-direction: column; - margin: 0 0.75rem 0 0; -} - -.tui-radio-group.disabled { - opacity: 0.5; -} - -.radio-label { - display: flex; - align-items: center; - gap: 0.35rem; - margin-bottom: 0.5rem; - color: var(--terminal-muted); - font-size: 0.85rem; -} - -:global(.label-icon) { - color: var(--radio-color); -} - -.radio-options { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.radio-options.horizontal { - flex-direction: row; - flex-wrap: wrap; - gap: 0.75rem; -} - -.radio-option { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.35rem 0.5rem; - cursor: pointer; - border-radius: 4px; - transition: all 0.15s ease; - user-select: none; -} - -.radio-option:hover:not(.option-disabled) { - background: color-mix(in srgb, var(--radio-color) 10%, transparent); -} - -.radio-option:focus-visible { - outline: 1px solid var(--radio-color); - outline-offset: 2px; -} - -.radio-option.option-disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.radio-symbol { - font-family: inherit; - font-weight: bold; - color: var(--terminal-muted); - transition: color 0.15s ease; -} - -.radio-option.selected .radio-symbol { - color: var(--radio-color); -} - -:global(.option-icon) { - color: var(--radio-color); -} - -.option-label { - color: var(--terminal-text); -} - -.radio-option.option-disabled .option-label { - color: var(--terminal-muted); -} diff --git a/src/lib/assets/css/tui-select.css b/src/lib/assets/css/tui-select.css deleted file mode 100644 index 9a2d038..0000000 --- a/src/lib/assets/css/tui-select.css +++ /dev/null @@ -1,162 +0,0 @@ -.tui-select { - position: relative; - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.tui-select.inline { - display: inline-flex; - flex-direction: column; - margin: 0 0.5rem 0 0; -} - -.select-label { - display: flex; - align-items: center; - gap: 0.35rem; - margin-bottom: 0.35rem; - color: var(--terminal-muted); - font-size: 0.85rem; -} - -:global(.label-icon) { - color: var(--select-color); -} - -.select-trigger { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.5rem 0.75rem; - background: color-mix(in srgb, var(--terminal-bg) 80%, black); - border: 1px solid var(--terminal-muted); - border-radius: 4px; - cursor: pointer; - transition: all 0.15s ease; -} - -.tui-select.open .select-trigger, -.select-trigger:focus-visible { - border-color: var(--select-color); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--select-color) 30%, transparent); -} - -.tui-select.error .select-trigger { - border-color: #f38ba8; -} - -.tui-select.disabled .select-trigger { - opacity: 0.5; - cursor: not-allowed; -} - -.select-prompt { - color: var(--select-color); - font-weight: bold; -} - -.select-value { - flex: 1; - color: var(--terminal-text); -} - -.select-value.placeholder { - color: var(--terminal-muted); - opacity: 0.6; -} - -.select-arrow { - color: var(--terminal-muted); - font-size: 0.7rem; -} - -.select-dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - margin-top: 0.25rem; - background: var(--terminal-bg); - border: 1px solid var(--select-color); - border-radius: 4px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - z-index: 100; - overflow: hidden; -} - -.select-search { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--terminal-muted); -} - -.select-search input { - flex: 1; - background: transparent; - border: none; - color: var(--terminal-text); - font-family: inherit; - font-size: inherit; - outline: none; -} - -.select-search input::placeholder { - color: var(--terminal-muted); -} - -.select-options { - max-height: 200px; - overflow-y: auto; -} - -.select-option { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - cursor: pointer; - transition: background 0.1s ease; -} - -.select-option.highlighted { - background: color-mix(in srgb, var(--select-color) 15%, transparent); -} - -.select-option.selected { - color: var(--select-color); -} - -.select-option.option-disabled { - opacity: 0.5; - cursor: not-allowed; -} - -:global(.option-icon) { - color: var(--select-color); -} - -.option-label { - flex: 1; -} - -:global(.check-icon) { - color: var(--select-color); -} - -.select-empty { - padding: 0.75rem; - text-align: center; - color: var(--terminal-muted); - font-style: italic; -} - -.select-error { - display: flex; - align-items: center; - gap: 0.35rem; - margin-top: 0.35rem; - color: #f38ba8; - font-size: 0.8rem; -} diff --git a/src/lib/assets/css/tui-table.css b/src/lib/assets/css/tui-table.css deleted file mode 100644 index 6081cde..0000000 --- a/src/lib/assets/css/tui-table.css +++ /dev/null @@ -1,56 +0,0 @@ -.tui-table-wrapper { - margin: 0.5rem 0; - border: 1px solid var(--terminal-border); - border-radius: 6px; - overflow: hidden; -} - -.tui-table-wrapper.inline { - display: inline-block; - width: auto; - vertical-align: top; - margin: 0 0.5rem 0 0; -} - -.table-title { - padding: 0.5rem 0.75rem; - font-weight: 600; - font-size: 0.85rem; - color: var(--terminal-text); - background: rgba(0, 0, 0, 0.1); - border-bottom: 1px solid var(--terminal-border); -} - -.tui-table { - width: 100%; - border-collapse: collapse; - font-size: 0.8rem; -} - -th { - padding: 0.5rem 0.75rem; - text-align: left; - font-weight: 600; - color: var(--table-accent); - background: rgba(0, 0, 0, 0.15); - border-bottom: 1px solid var(--terminal-border); - white-space: nowrap; -} - -td { - padding: 0.4rem 0.75rem; - color: var(--terminal-text); - border-bottom: 1px solid var(--terminal-border); -} - -tr:last-child td { - border-bottom: none; -} - -tr.alt { - background: rgba(0, 0, 0, 0.03); -} - -tr:hover { - background: color-mix(in srgb, var(--table-accent) 5%, transparent); -} \ No newline at end of file diff --git a/src/lib/assets/css/tui-textarea.css b/src/lib/assets/css/tui-textarea.css deleted file mode 100644 index 1f4a673..0000000 --- a/src/lib/assets/css/tui-textarea.css +++ /dev/null @@ -1,113 +0,0 @@ -.tui-textarea { - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.tui-textarea.inline { - display: inline-flex; - flex-direction: column; - margin: 0 0.5rem 0 0; -} - -.textarea-label { - display: flex; - align-items: center; - gap: 0.35rem; - margin-bottom: 0.35rem; - color: var(--terminal-muted); - font-size: 0.85rem; -} - -:global(.label-icon) { - color: var(--input-color); -} - -.textarea-wrapper { - display: flex; - background: color-mix(in srgb, var(--terminal-bg) 80%, black); - border: 1px solid var(--terminal-muted); - border-radius: 4px; - transition: all 0.15s ease; - overflow: hidden; -} - -.tui-textarea.focused .textarea-wrapper { - border-color: var(--input-color); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent); -} - -.tui-textarea.error .textarea-wrapper { - border-color: #f38ba8; -} - -.tui-textarea.disabled .textarea-wrapper { - opacity: 0.5; - cursor: not-allowed; -} - -.line-numbers { - display: flex; - flex-direction: column; - padding: 0.5rem 0; - background: color-mix(in srgb, var(--terminal-bg) 60%, black); - border-right: 1px solid var(--terminal-muted); - user-select: none; -} - -.line-num { - padding: 0 0.5rem; - color: var(--terminal-muted); - font-size: 0.8rem; - line-height: 1.5; - text-align: right; - min-width: 2rem; -} - -textarea { - flex: 1; - min-width: 0; - padding: 0.5rem 0.75rem; - background: transparent; - border: none; - color: var(--terminal-text); - font-family: inherit; - font-size: inherit; - line-height: 1.5; - resize: vertical; - outline: none; -} - -textarea::placeholder { - color: var(--terminal-muted); - opacity: 0.6; -} - -textarea:disabled { - cursor: not-allowed; - resize: none; -} - -.textarea-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 0.35rem; -} - -.textarea-error { - display: flex; - align-items: center; - gap: 0.35rem; - color: #f38ba8; - font-size: 0.8rem; -} - -.char-count { - margin-left: auto; - color: var(--terminal-muted); - font-size: 0.75rem; -} - -.char-count.warning { - color: #f9e2af; -} diff --git a/src/lib/assets/css/tui-toggle.css b/src/lib/assets/css/tui-toggle.css deleted file mode 100644 index 1e04a4c..0000000 --- a/src/lib/assets/css/tui-toggle.css +++ /dev/null @@ -1,94 +0,0 @@ -.tui-toggle { - display: flex; - align-items: center; - gap: 0.75rem; - margin: 0.35rem 0; - padding: 0.35rem 0.5rem; - font-size: 0.9rem; - cursor: pointer; - border-radius: 4px; - transition: all 0.15s ease; - user-select: none; -} - -.tui-toggle.inline { - display: inline-flex; - margin: 0 0.75rem 0 0; -} - -.tui-toggle:hover:not(.disabled) { - background: color-mix(in srgb, var(--toggle-color) 10%, transparent); -} - -.tui-toggle:focus-visible { - outline: 1px solid var(--toggle-color); - outline-offset: 2px; -} - -.tui-toggle.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -:global(.toggle-icon) { - color: var(--toggle-color); -} - -.toggle-label { - color: var(--terminal-text); -} - -.toggle-track { - display: flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.5rem; - background: color-mix(in srgb, var(--terminal-bg) 80%, black); - border: 1px solid var(--terminal-muted); - border-radius: 4px; - font-size: 0.75rem; - transition: all 0.15s ease; -} - -.tui-toggle.checked .toggle-track { - border-color: var(--toggle-color); - background: color-mix(in srgb, var(--toggle-color) 15%, transparent); -} - -.toggle-off-label, -.toggle-on-label { - color: var(--terminal-muted); - font-weight: bold; - text-transform: uppercase; - min-width: 2rem; - text-align: center; - transition: color 0.15s ease; -} - -.tui-toggle:not(.checked) .toggle-off-label { - color: var(--terminal-text); -} - -.tui-toggle.checked .toggle-on-label { - color: var(--toggle-color); -} - -.toggle-switch { - display: flex; - align-items: center; - justify-content: center; - width: 2.5rem; - position: relative; -} - -.toggle-knob { - font-size: 1rem; - color: var(--terminal-muted); - transition: all 0.2s ease; - transform: translateX(-0.5rem); -} - -.tui-toggle.checked .toggle-knob { - color: var(--toggle-color); - transform: translateX(0.5rem); -} diff --git a/src/lib/assets/css/tui-tooltip.css b/src/lib/assets/css/tui-tooltip.css deleted file mode 100644 index d50cd20..0000000 --- a/src/lib/assets/css/tui-tooltip.css +++ /dev/null @@ -1,78 +0,0 @@ -.tui-tooltip-trigger { - position: relative; - display: inline-flex; - align-items: center; - gap: 0.25rem; - cursor: help; -} - -.trigger-text { - border-bottom: 1px dotted var(--tooltip-color); -} - -.tooltip-indicator { - font-size: 0.7rem; - color: var(--tooltip-color); - opacity: 0.7; -} - -.tooltip { - position: fixed; - z-index: 99999; - padding: 0.5rem 0.75rem; - background: var(--terminal-bg); - border: 1px solid var(--tooltip-color); - border-radius: 4px; - font-size: 0.8rem; - color: var(--terminal-text); - white-space: nowrap; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - animation: tooltipFadeIn 0.15s ease-out; - pointer-events: none; -} - -@keyframes tooltipFadeIn { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - -/* Arrow styles - simplified for fixed positioning */ -.tooltip-arrow { - position: absolute; - width: 8px; - height: 8px; - background: var(--terminal-bg); - border: 1px solid var(--tooltip-color); - border-top: none; - border-left: none; -} - -.tooltip.top .tooltip-arrow { - bottom: -5px; - left: 50%; - transform: translateX(-50%) rotate(45deg); -} - -.tooltip.bottom .tooltip-arrow { - top: -5px; - left: 50%; - transform: translateX(-50%) rotate(-135deg); -} - -.tooltip.left .tooltip-arrow { - right: -5px; - top: 50%; - transform: translateY(-50%) rotate(-45deg); -} - -.tooltip.right .tooltip-arrow { - left: -5px; - top: 50%; - transform: translateY(-50%) rotate(135deg); -} diff --git a/src/lib/assets/themes/arch.theme.json b/src/lib/assets/themes/arch.theme.json deleted file mode 100644 index c68f048..0000000 --- a/src/lib/assets/themes/arch.theme.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "Arch Linux", - "icon": "🐧", - "dark": { - "colors": { - "primary": "#1793d1", - "secondary": "#21262d", - "accent": "#23d18b", - "background": "#0d1117", - "backgroundLight": "#161b22", - "text": "#c9d1d9", - "textMuted": "#8b949e", - "border": "#484f58", - "terminal": "#0d1117", - "terminalPrompt": "#1793d1", - "terminalUser": "#23d18b", - "terminalPath": "#1793d1" - }, - "colorMap": { - "red": "#ff7b72", - "green": "#3fb950", - "yellow": "#d29922", - "blue": "#58a6ff", - "magenta": "#bc8cff", - "cyan": "#39c5cf", - "white": "#f0f6fc", - "gray": "#6e7681", - "orange": "#ffa657", - "pink": "#ff79c6", - "black": "#484f58", - "surface": "#21262d", - "teal": "#39c5cf", - "sky": "#58a6ff", - "sapphire": "#1793d1", - "lavender": "#bc8cff", - "peach": "#ffa657", - "maroon": "#ff7b72", - "mauve": "#bc8cff", - "flamingo": "#e06c75", - "rosewater": "#e8b4b8", - "error": "#ff7b72", - "success": "#3fb950", - "warning": "#d29922", - "info": "#1793d1" - } - }, - "light": { - "colors": { - "primary": "#1793d1", - "secondary": "#ddf4ff", - "accent": "#0969da", - "background": "#ffffff", - "backgroundLight": "#f6f8fa", - "text": "#24292f", - "textMuted": "#57606a", - "border": "#d0d7de", - "terminal": "#ffffff", - "terminalPrompt": "#1793d1", - "terminalUser": "#0969da", - "terminalPath": "#1793d1" - }, - "colorMap": { - "red": "#cf222e", - "green": "#1a7f37", - "yellow": "#9a6700", - "blue": "#0969da", - "magenta": "#8250df", - "cyan": "#1b7c83", - "white": "#24292f", - "gray": "#6e7781", - "orange": "#bc4c00", - "pink": "#bf3989", - "black": "#24292f", - "surface": "#f6f8fa", - "teal": "#1b7c83", - "sky": "#0969da", - "sapphire": "#1793d1", - "lavender": "#8250df", - "peach": "#bc4c00", - "maroon": "#cf222e", - "mauve": "#8250df", - "flamingo": "#bf3989", - "rosewater": "#d0d7de", - "error": "#cf222e", - "success": "#1a7f37", - "warning": "#9a6700", - "info": "#0969da" - } - } -} \ No newline at end of file diff --git a/src/lib/assets/themes/catppuccin.theme.json b/src/lib/assets/themes/catppuccin.theme.json deleted file mode 100644 index 949e94c..0000000 --- a/src/lib/assets/themes/catppuccin.theme.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "Catppuccin", - "icon": "🐱", - "dark": { - "colors": { - "primary": "#89b4fa", - "secondary": "#585b70", - "accent": "#a6e3a1", - "background": "#1e1e2e", - "backgroundLight": "#313244", - "text": "#cdd6f4", - "textMuted": "#a6adc8", - "border": "#6c7086", - "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": "#45475a", - "surface": "#313244", - "teal": "#94e2d5", - "sky": "#89dceb", - "sapphire": "#74c7ec", - "lavender": "#b4befe", - "peach": "#fab387", - "maroon": "#eba0ac", - "mauve": "#cba6f7", - "flamingo": "#f2cdcd", - "rosewater": "#f5e0dc", - "error": "#f38ba8", - "success": "#a6e3a1", - "warning": "#f9e2af", - "info": "#89b4fa" - } - }, - "light": { - "colors": { - "primary": "#1e66f5", - "secondary": "#acb0be", - "accent": "#40a02b", - "background": "#eff1f5", - "backgroundLight": "#e6e9ef", - "text": "#4c4f69", - "textMuted": "#6c6f85", - "border": "#9ca0b0", - "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": "#bcc0cc", - "surface": "#ccd0da", - "teal": "#179299", - "sky": "#04a5e5", - "sapphire": "#209fb5", - "lavender": "#7287fd", - "peach": "#fe640b", - "maroon": "#e64553", - "mauve": "#8839ef", - "flamingo": "#dd7878", - "rosewater": "#dc8a78", - "error": "#d20f39", - "success": "#40a02b", - "warning": "#df8e1d", - "info": "#1e66f5" - } - } -} \ No newline at end of file diff --git a/src/lib/assets/themes/cerberus.theme.json b/src/lib/assets/themes/cerberus.theme.json deleted file mode 100644 index 6525410..0000000 --- a/src/lib/assets/themes/cerberus.theme.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "Cerberus", - "icon": "🐕", - "dark": { - "colors": { - "primary": "#818cf8", - "secondary": "#e879f9", - "accent": "#2dd4bf", - "background": "#171717", - "backgroundLight": "#262626", - "text": "#f5f5f5", - "textMuted": "#a3a3a3", - "border": "#404040", - "terminal": "#171717", - "terminalPrompt": "#818cf8", - "terminalUser": "#2dd4bf", - "terminalPath": "#e879f9" - }, - "colorMap": { - "red": "#ef4444", - "green": "#2dd4bf", - "yellow": "#fbbf24", - "blue": "#818cf8", - "magenta": "#e879f9", - "cyan": "#67e8f9", - "white": "#f5f5f5", - "gray": "#737373", - "orange": "#fb923c", - "pink": "#f472b6", - "black": "#404040", - "surface": "#262626", - "teal": "#2dd4bf", - "sky": "#38bdf8", - "sapphire": "#3b82f6", - "lavender": "#a78bfa", - "peach": "#fdba74", - "maroon": "#f43f5e", - "mauve": "#c084fc", - "flamingo": "#f472b6", - "rosewater": "#fb7185", - "error": "#ef4444", - "success": "#2dd4bf", - "warning": "#fbbf24", - "info": "#818cf8" - } - }, - "light": { - "colors": { - "primary": "#4f46e5", - "secondary": "#c026d3", - "accent": "#0d9488", - "background": "#ffffff", - "backgroundLight": "#f5f5f5", - "text": "#171717", - "textMuted": "#525252", - "border": "#d4d4d4", - "terminal": "#ffffff", - "terminalPrompt": "#4f46e5", - "terminalUser": "#0d9488", - "terminalPath": "#c026d3" - }, - "colorMap": { - "red": "#dc2626", - "green": "#0f766e", - "yellow": "#d97706", - "blue": "#4f46e5", - "magenta": "#c026d3", - "cyan": "#0891b2", - "white": "#171717", - "gray": "#525252", - "orange": "#ea580c", - "pink": "#db2777", - "black": "#262626", - "surface": "#f5f5f5", - "teal": "#0f766e", - "sky": "#0284c7", - "sapphire": "#1d4ed8", - "lavender": "#7e22ce", - "peach": "#f97316", - "maroon": "#e11d48", - "mauve": "#9333ea", - "flamingo": "#be185d", - "rosewater": "#e11d48", - "error": "#dc2626", - "success": "#0f766e", - "warning": "#d97706", - "info": "#4f46e5" - } - } -} \ No newline at end of file diff --git a/src/lib/assets/themes/rose.theme.json b/src/lib/assets/themes/rose.theme.json deleted file mode 100644 index 2cb3a08..0000000 --- a/src/lib/assets/themes/rose.theme.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "Rose", - "icon": "🌹", - "dark": { - "colors": { - "primary": "#fb7185", - "secondary": "#c084fc", - "accent": "#2dd4bf", - "background": "#1f181b", - "backgroundLight": "#2e2327", - "text": "#ffe4e6", - "textMuted": "#fda4af", - "border": "#5f484f", - "terminal": "#1f181b", - "terminalPrompt": "#fb7185", - "terminalUser": "#2dd4bf", - "terminalPath": "#c084fc" - }, - "colorMap": { - "red": "#f43f5e", - "green": "#34d399", - "yellow": "#fde047", - "blue": "#60a5fa", - "magenta": "#e879f9", - "cyan": "#67e8f9", - "white": "#ffe4e6", - "gray": "#886e75", - "orange": "#fb923c", - "pink": "#fb7185", - "black": "#5f484f", - "surface": "#2e2327", - "teal": "#2dd4bf", - "sky": "#7dd3fc", - "sapphire": "#3b82f6", - "lavender": "#c084fc", - "peach": "#fdba74", - "maroon": "#be123c", - "mauve": "#d8b4fe", - "flamingo": "#f472b6", - "rosewater": "#fecdd3", - "error": "#f43f5e", - "success": "#34d399", - "warning": "#fde047", - "info": "#60a5fa" - } - }, - "light": { - "colors": { - "primary": "#be123c", - "secondary": "#7e22ce", - "accent": "#0f766e", - "background": "#fff1f2", - "backgroundLight": "#ffe4e6", - "text": "#4c0519", - "textMuted": "#9f1239", - "border": "#fecdd3", - "terminal": "#fff1f2", - "terminalPrompt": "#be123c", - "terminalUser": "#0f766e", - "terminalPath": "#7e22ce" - }, - "colorMap": { - "red": "#e11d48", - "green": "#059669", - "yellow": "#d97706", - "blue": "#2563eb", - "magenta": "#9333ea", - "cyan": "#0891b2", - "white": "#4c0519", - "gray": "#9f1239", - "orange": "#ea580c", - "pink": "#db2777", - "black": "#881337", - "surface": "#ffe4e6", - "teal": "#0f766e", - "sky": "#0284c7", - "sapphire": "#1d4ed8", - "lavender": "#7e22ce", - "peach": "#f97316", - "maroon": "#881337", - "mauve": "#a855f7", - "flamingo": "#be185d", - "rosewater": "#e11d48", - "error": "#e11d48", - "success": "#059669", - "warning": "#d97706", - "info": "#2563eb" - } - } -} \ No newline at end of file diff --git a/src/lib/assets/themes/wintry.theme.json b/src/lib/assets/themes/wintry.theme.json deleted file mode 100644 index 2be039b..0000000 --- a/src/lib/assets/themes/wintry.theme.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "Wintry", - "icon": "❄️", - "dark": { - "colors": { - "primary": "#7dd3fc", - "secondary": "#334155", - "accent": "#2dd4bf", - "background": "#0f172a", - "backgroundLight": "#1e293b", - "text": "#f1f5f9", - "textMuted": "#94a3b8", - "border": "#475569", - "terminal": "#0f172a", - "terminalPrompt": "#7dd3fc", - "terminalUser": "#2dd4bf", - "terminalPath": "#818cf8" - }, - "colorMap": { - "red": "#f43f5e", - "green": "#4ade80", - "yellow": "#fde047", - "blue": "#60a5fa", - "magenta": "#c084fc", - "cyan": "#67e8f9", - "white": "#f1f5f9", - "gray": "#64748b", - "orange": "#fb923c", - "pink": "#f472b6", - "black": "#334155", - "surface": "#1e293b", - "teal": "#2dd4bf", - "sky": "#7dd3fc", - "sapphire": "#3b82f6", - "lavender": "#a78bfa", - "peach": "#fdba74", - "maroon": "#fda4af", - "mauve": "#d8b4fe", - "flamingo": "#f9a8d4", - "rosewater": "#fecdd3", - "error": "#f43f5e", - "success": "#4ade80", - "warning": "#fde047", - "info": "#60a5fa" - } - }, - "light": { - "colors": { - "primary": "#0284c7", - "secondary": "#e2e8f0", - "accent": "#0d9488", - "background": "#f8fafc", - "backgroundLight": "#ffffff", - "text": "#0f172a", - "textMuted": "#64748b", - "border": "#cbd5e1", - "terminal": "#ffffff", - "terminalPrompt": "#0284c7", - "terminalUser": "#0d9488", - "terminalPath": "#4f46e5" - }, - "colorMap": { - "red": "#e11d48", - "green": "#16a34a", - "yellow": "#d97706", - "blue": "#2563eb", - "magenta": "#9333ea", - "cyan": "#0891b2", - "white": "#0f172a", - "gray": "#64748b", - "orange": "#ea580c", - "pink": "#db2777", - "black": "#1e293b", - "surface": "#e2e8f0", - "teal": "#0d9488", - "sky": "#0284c7", - "sapphire": "#1d4ed8", - "lavender": "#7c3aed", - "peach": "#f97316", - "maroon": "#be123c", - "mauve": "#a855f7", - "flamingo": "#be185d", - "rosewater": "#e11d48", - "error": "#e11d48", - "success": "#16a34a", - "warning": "#d97706", - "info": "#2563eb" - } - } -} \ No newline at end of file diff --git a/src/lib/blog/posts.ts b/src/lib/blog/posts.ts index 592f2ae..bb4f331 100644 --- a/src/lib/blog/posts.ts +++ b/src/lib/blog/posts.ts @@ -1,19 +1,27 @@ -import { user } from '$lib/config'; +import { profile } from '#lib/content'; import { parseFrontmatter } from './frontmatter'; import { renderMarkdown } from './markdown'; -export interface BlogPost { +export interface BlogPostSummary { slug: string; title: string; author: string; date: string; tags: string[]; excerpt: string; - html: string; readingTime: number; } -const postFiles = import.meta.glob('/blogs/*.md', { +export interface BlogPost extends BlogPostSummary { + html: string; +} + +export interface BlogPostLink { + slug: string; + title: string; +} + +const postFiles = import.meta.glob('/content/blog/*.md', { eager: true, query: '?raw', import: 'default' @@ -24,8 +32,8 @@ function slugFromPath(path: string): string { } function estimateReadingTime(content: string): number { - const words = content.trim().split(/\s+/).filter(Boolean).length; - return Math.max(1, Math.round(words / 200)); + const wordCount = content.trim().split(/\s+/).filter(Boolean).length; + return Math.max(1, Math.round(wordCount / 200)); } function buildPost(path: string, raw: string): BlogPost { @@ -35,7 +43,7 @@ function buildPost(path: string, raw: string): BlogPost { return { slug, title: (data.title as string) || slug, - author: (data.author as string) || user.displayname, + author: (data.author as string) || profile.displayName, date: (data.date as string) || '', tags: (data.tags as string[]) || [], excerpt: (data.excerpt as string) || '', @@ -46,27 +54,21 @@ function buildPost(path: string, raw: string): BlogPost { const allPosts: BlogPost[] = Object.entries(postFiles) .map(([path, raw]) => buildPost(path, raw)) - .sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); + .sort((first, second) => second.date.localeCompare(first.date)); export function getAllPosts(): BlogPost[] { return allPosts; } +export function getPostSummaries(): BlogPostSummary[] { + return allPosts.map(({ html, ...summary }) => summary); +} + export function getPostBySlug(slug: string): BlogPost | undefined { return allPosts.find((post) => post.slug === slug); } -export interface BlogPostNav { - slug: string; - title: string; -} - -export interface AdjacentPosts { - previous: BlogPostNav | null; - next: BlogPostNav | null; -} - -export function getAdjacentPosts(slug: string): AdjacentPosts { +export function getAdjacentPosts(slug: string): { previous: BlogPostLink | null; next: BlogPostLink | null } { const index = allPosts.findIndex((post) => post.slug === slug); if (index === -1) return { previous: null, next: null }; @@ -80,9 +82,5 @@ export function getAdjacentPosts(slug: string): AdjacentPosts { } export function getAllTags(): string[] { - const tags = new Set(); - for (const post of allPosts) { - for (const tag of post.tags) tags.add(tag); - } - return Array.from(tags).sort(); + return [...new Set(allPosts.flatMap((post) => post.tags))].sort(); } diff --git a/src/lib/components/Background3D.svelte b/src/lib/components/Background3D.svelte deleted file mode 100644 index 85846e6..0000000 --- a/src/lib/components/Background3D.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - -
- {#if webglAvailable} - - - - - - - - - - - - - - - - {/if} -
diff --git a/src/lib/components/ModelViewer.svelte b/src/lib/components/ModelViewer.svelte deleted file mode 100644 index 9e27983..0000000 --- a/src/lib/components/ModelViewer.svelte +++ /dev/null @@ -1,624 +0,0 @@ - - -
- -
-
- - {modelName} -
-
- - - - -
-
- - - {#if showControls} -
-
- View -
- - -
-
-
- Lighting -
- - {Math.round(lightIntensity * 100)}% - -
-
-
- Display -
- - - - -
-
-
- Rotation -
- - {rotationSpeed.toFixed(1)} - -
-
-
- {/if} - - - -
- {#if isLoading} -
- - Loading model... -
- {/if} - {#if loadError} -
- - {loadError} -
- {/if} -
- - - -
diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte deleted file mode 100644 index 5d8ebaa..0000000 --- a/src/lib/components/Navbar.svelte +++ /dev/null @@ -1,144 +0,0 @@ - - - - - diff --git a/src/lib/components/NavbarWaybar.svelte b/src/lib/components/NavbarWaybar.svelte deleted file mode 100644 index 5cd4deb..0000000 --- a/src/lib/components/NavbarWaybar.svelte +++ /dev/null @@ -1,532 +0,0 @@ - - - - - - - -{#if mobileMenuOpen} - -{/if} - - -{#if mobileMenuOpen} -
- - -
- -
-
Theme
-
- {#each themeOptions as option, i} - - {/each} -
- -
-
-{/if} diff --git a/src/lib/components/ParticleField.svelte b/src/lib/components/ParticleField.svelte deleted file mode 100644 index 8cafb86..0000000 --- a/src/lib/components/ParticleField.svelte +++ /dev/null @@ -1,111 +0,0 @@ - - -{#if particles} - -{/if} diff --git a/src/lib/components/Terminal.svelte b/src/lib/components/Terminal.svelte deleted file mode 100644 index 76168e3..0000000 --- a/src/lib/components/Terminal.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - -
- -
-
- - - -
-
- 🐧 - arch@terminal: ~ -
-
-
- - -
- {#each displayedLines as { line, text, complete }, i} -
- {#if line.type === 'command' || line.type === 'prompt'} - - user@arch:~$ - - {/if} - - {getLinePrefix(line.type)}{text} - - {#if showCursor && i === currentLineIndex && !complete && isTyping} - - {/if} -
- {/each} - - {#if showCursor && !isTyping && displayedLines.length > 0} -
- - user@arch:~$ - - -
- {/if} -
-
diff --git a/src/lib/components/TerminalPage.svelte b/src/lib/components/TerminalPage.svelte deleted file mode 100644 index d2ea9f1..0000000 --- a/src/lib/components/TerminalPage.svelte +++ /dev/null @@ -1,221 +0,0 @@ - - -
- {#if showHeader} - -
-
- - - -
-
- 🐧 - {getPromptString()}: {title} -
-
-
- {/if} - - -
- {#each displayedLines as { line, text, complete, showImage }, i} -
- {#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'} - {text} - {:else if line.type !== 'blank'} - - {getLinePrefix(line.type)}{text} - - {/if} - - {#if terminalSettings.showCursor && i === currentLineIndex && !complete && isTyping && line.type !== 'image' && line.type !== 'blank'} - - {/if} -
- {/each} - - {#if terminalSettings.showCursor && !isTyping && isComplete} -
- - {user.username}@{user.hostname}:~$ - - -
- {/if} -
-
diff --git a/src/lib/components/TerminalTUI.svelte b/src/lib/components/TerminalTUI.svelte deleted file mode 100644 index d279154..0000000 --- a/src/lib/components/TerminalTUI.svelte +++ /dev/null @@ -1,358 +0,0 @@ - - - - -
- -
- - -
- 0} - /> - - - (selectedIndex = i)} - onLinkClick={handleLinkClick} - {terminalSettings} - interactiveInput={enableCommands && interactive} - onCommand={handleCommand} - /> - - -
-
diff --git a/src/lib/components/blog/PostContent.svelte b/src/lib/components/blog/PostContent.svelte new file mode 100644 index 0000000..8979e6d --- /dev/null +++ b/src/lib/components/blog/PostContent.svelte @@ -0,0 +1,137 @@ + + +
+ {@html html} +
+ + diff --git a/src/lib/components/desktop/Desktop.svelte b/src/lib/components/desktop/Desktop.svelte new file mode 100644 index 0000000..a6f9246 --- /dev/null +++ b/src/lib/components/desktop/Desktop.svelte @@ -0,0 +1,162 @@ + + +
+ {#if isNarrow && openWindowIds.length > 1} + + {/if} + +
+ +
+ +
+ {#if pageOpen} + windowManager.focus(pageWindowId)} + onclose={closePage} + > + {@render children()} + + {/if} + + {#each windowManager.terminals as terminal (terminal.id)} + windowManager.focus(terminal.id)} + onclose={() => windowManager.closeTerminal(terminal.id)} + > + windowManager.closeTerminal(terminal.id)} + /> + + {/each} +
+
+ + diff --git a/src/lib/components/desktop/DesktopIcons.svelte b/src/lib/components/desktop/DesktopIcons.svelte new file mode 100644 index 0000000..d78a3d3 --- /dev/null +++ b/src/lib/components/desktop/DesktopIcons.svelte @@ -0,0 +1,84 @@ + + + + + diff --git a/src/lib/components/desktop/Launcher.svelte b/src/lib/components/desktop/Launcher.svelte new file mode 100644 index 0000000..64e0704 --- /dev/null +++ b/src/lib/components/desktop/Launcher.svelte @@ -0,0 +1,175 @@ + + + + + + diff --git a/src/lib/components/desktop/Window.svelte b/src/lib/components/desktop/Window.svelte new file mode 100644 index 0000000..e89641c --- /dev/null +++ b/src/lib/components/desktop/Window.svelte @@ -0,0 +1,148 @@ + + +
+
+
+ + {title} + {#if path} + {path} + {/if} + + +
+
+ {@render children()} +
+
+
+ + diff --git a/src/lib/components/models/ModelViewer.svelte b/src/lib/components/models/ModelViewer.svelte new file mode 100644 index 0000000..b1fec12 --- /dev/null +++ b/src/lib/components/models/ModelViewer.svelte @@ -0,0 +1,149 @@ + + +
+
+ {modelName} +
+ + + + + +
+
+
+ {#if status !== 'ready'} +
+ {#if status === 'loading'} + loading model... + {:else if status === 'error'} + failed to load model + {:else} + WebGL is not available in this browser + {/if} +
+ {/if} +
+
drag to rotate · scroll to zoom · arrow keys after clicking
+
+ + diff --git a/src/lib/components/projects/ProjectEntry.svelte b/src/lib/components/projects/ProjectEntry.svelte new file mode 100644 index 0000000..28b0f43 --- /dev/null +++ b/src/lib/components/projects/ProjectEntry.svelte @@ -0,0 +1,184 @@ + + +
+

{project.name}

+ + {#if project.image} + {project.name} + {/if} + +
+
+ {category.title} + {#if project.featured} + Featured + {/if} + {#if project.version} + v{project.version} + {/if} +
+ + {#if project.event} +
+ {project.event.name}{#if project.event.year} ({project.event.year}){/if} +
+ {#if project.event.university} +
{project.event.university}{project.event.location ? `, ${project.event.location}` : ''}
+ {/if} + {/if} + + {#if project.organization} +
{project.organization}
+ {/if} + {#if project.period} +
{project.period}
+ {/if} + + {#if project.description} +

{project.description}

+ {/if} + + {#each project.awards ?? [] as award (award.track)} +
{award.place} - {award.track}
+ {/each} + + {#if project.tags.length} +
+ {#each project.tags as tag, index (tag)} + + {/each} +
+ {/if} + + {#if project.demoMayBeOffline} +
Demo may be unavailable
+ {/if} + + {#if linkButtons.length} +
+ {#each linkButtons as button (button.label)} + {button.label} + {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/projects/ProjectNav.svelte b/src/lib/components/projects/ProjectNav.svelte new file mode 100644 index 0000000..d232831 --- /dev/null +++ b/src/lib/components/projects/ProjectNav.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/src/lib/components/shell/Bar.svelte b/src/lib/components/shell/Bar.svelte new file mode 100644 index 0000000..6183e3b --- /dev/null +++ b/src/lib/components/shell/Bar.svelte @@ -0,0 +1,84 @@ + + +
+
+ + +
+ + + +
+ + + +
+
+ + diff --git a/src/lib/components/shell/Battery.svelte b/src/lib/components/shell/Battery.svelte new file mode 100644 index 0000000..097c6d0 --- /dev/null +++ b/src/lib/components/shell/Battery.svelte @@ -0,0 +1,50 @@ + + +{#if battery} + + + {battery.level}% + +{/if} + + diff --git a/src/lib/components/shell/ChaiIcon.svelte b/src/lib/components/shell/ChaiIcon.svelte new file mode 100644 index 0000000..41e649e --- /dev/null +++ b/src/lib/components/shell/ChaiIcon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/shell/Clock.svelte b/src/lib/components/shell/Clock.svelte new file mode 100644 index 0000000..624e574 --- /dev/null +++ b/src/lib/components/shell/Clock.svelte @@ -0,0 +1,42 @@ + + +{now ? formatClock(now) : ''} +{now ? formatClock(now).split(' ').slice(1).join(' ') : ''} + + diff --git a/src/lib/components/shell/DiscordPopover.svelte b/src/lib/components/shell/DiscordPopover.svelte new file mode 100644 index 0000000..05c64c2 --- /dev/null +++ b/src/lib/components/shell/DiscordPopover.svelte @@ -0,0 +1,294 @@ + + + + + + + + diff --git a/src/lib/components/shell/DiscordStatus.svelte b/src/lib/components/shell/DiscordStatus.svelte new file mode 100644 index 0000000..a68281f --- /dev/null +++ b/src/lib/components/shell/DiscordStatus.svelte @@ -0,0 +1,97 @@ + + +
+ + + {#if open} + (open = false)} /> + {/if} +
+ + diff --git a/src/lib/components/shell/PopupCard.svelte b/src/lib/components/shell/PopupCard.svelte new file mode 100644 index 0000000..458d174 --- /dev/null +++ b/src/lib/components/shell/PopupCard.svelte @@ -0,0 +1,58 @@ + + + + + + + + diff --git a/src/lib/components/shell/ThemeMenu.svelte b/src/lib/components/shell/ThemeMenu.svelte new file mode 100644 index 0000000..8cd863c --- /dev/null +++ b/src/lib/components/shell/ThemeMenu.svelte @@ -0,0 +1,79 @@ + + +
+ + + {#if open} + (open = false)}> + {#each themes as theme (theme.id)} + + {/each} + + {/if} +
+ + diff --git a/src/lib/components/shell/Workspaces.svelte b/src/lib/components/shell/Workspaces.svelte new file mode 100644 index 0000000..06962de --- /dev/null +++ b/src/lib/components/shell/Workspaces.svelte @@ -0,0 +1,81 @@ + + +
+ {#each workspaceSlots as number (number)} + {@const workspace = workspaceFor(number)} + {@const isFocused = currentWorkspace?.number === number} + + {/each} +
+ + diff --git a/src/lib/components/terminal/Fetch.svelte b/src/lib/components/terminal/Fetch.svelte new file mode 100644 index 0000000..169de09 --- /dev/null +++ b/src/lib/components/terminal/Fetch.svelte @@ -0,0 +1,85 @@ + + +
+ {profile.displayName} +
+
+ {profile.username}@{profile.hostname} +
+
{'-'.repeat(profile.username.length + profile.hostname.length + 1)}
+ {#each rows as [label, value] (label)} +
{label}: {value}
+ {/each} +
+ {#each paletteColors as color (color)} + + {/each} +
+
+
+ + diff --git a/src/lib/components/terminal/OutputLine.svelte b/src/lib/components/terminal/OutputLine.svelte new file mode 100644 index 0000000..7811dfe --- /dev/null +++ b/src/lib/components/terminal/OutputLine.svelte @@ -0,0 +1,52 @@ + + +
+ {#each spans as item, index (index)} + {#if item.href} + { + if (isExternalUrl(item.href!)) return; + event.preventDefault(); + openUrl(item.href!); + }}>{item.text} + {:else} + {item.text} + {/if} + {/each} +
+ + diff --git a/src/lib/components/terminal/Terminal.svelte b/src/lib/components/terminal/Terminal.svelte new file mode 100644 index 0000000..79bbefa --- /dev/null +++ b/src/lib/components/terminal/Terminal.svelte @@ -0,0 +1,188 @@ + + + + + diff --git a/src/lib/components/tui/TuiAccordion.svelte b/src/lib/components/tui/TuiAccordion.svelte deleted file mode 100644 index 4371cd7..0000000 --- a/src/lib/components/tui/TuiAccordion.svelte +++ /dev/null @@ -1,186 +0,0 @@ - - -
- {#each items as item, i} - {@const contentSegments = parseColorText( - item.content, - $themeColors.colorMap, - )} -
- - {#if openItems.has(i)} -
- {#each contentSegments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - - {#if isSingleItemMode && line.children} -
- {#each line.children as child, k} - - {/each} -
- {/if} -
- {/if} -
- {/each} -
- - diff --git a/src/lib/components/tui/TuiBody.svelte b/src/lib/components/tui/TuiBody.svelte deleted file mode 100644 index 0f2627a..0000000 --- a/src/lib/components/tui/TuiBody.svelte +++ /dev/null @@ -1,313 +0,0 @@ - - - -
- {#each processedGroups as group, gi (gi)} - {#if group.kind === "inline"} -
- {#each group.items as item (item.index)} - - {/each} -
- {:else} - - {/if} - {/each} - - {#if interactiveInput} - {#if !isTyping} -
- - {user.username}@{user.hostname} - :~$ - - - { - inputFocused = true; - syncCaret(); - }} - onblur={() => (inputFocused = false)} - spellcheck="false" - autocomplete="off" - autocapitalize="off" - autocorrect="off" - aria-label="Terminal command input" - /> - -
- {/if} - {:else if terminalSettings.showCursor && !isTyping && displayedLines.length > 0} -
- - {user.username}@{user.hostname} - :~$ - - -
- {/if} -
- - diff --git a/src/lib/components/tui/TuiButton.svelte b/src/lib/components/tui/TuiButton.svelte deleted file mode 100644 index 6edec0d..0000000 --- a/src/lib/components/tui/TuiButton.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - diff --git a/src/lib/components/tui/TuiCard.svelte b/src/lib/components/tui/TuiCard.svelte deleted file mode 100644 index ee50b68..0000000 --- a/src/lib/components/tui/TuiCard.svelte +++ /dev/null @@ -1,291 +0,0 @@ - - -
- {#if line.cardTitle} -
- {#if line.icon} - - {/if} - - {#if line.link} - - {#each titleSegments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - - {:else} - {#each titleSegments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - {/if} - -
- {/if} - -
- {#if line.image} - {line.imageAlt - {/if} -
- {#each segments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} -
- {#if line.children && line.children.length > 0} -
- {#each processedChildren as group} - {#if group.kind === "inline"} -
- {#each group.items as item} - - {/each} -
- {:else} - - {/if} - {/each} -
- {/if} -
- - {#if line.cardFooter} - - {/if} -
- - diff --git a/src/lib/components/tui/TuiCardGrid.svelte b/src/lib/components/tui/TuiCardGrid.svelte deleted file mode 100644 index d5931b9..0000000 --- a/src/lib/components/tui/TuiCardGrid.svelte +++ /dev/null @@ -1,367 +0,0 @@ - - -
- {#each cards as card} -
- {#if card.image} -
- {card.title} -
- {:else if card.featured} -
- ★ Featured -
- {/if} - -
-

{card.title}

- - {#if card.hackathonName} -
- 🏛️ - {card.hackathonName} - {#if card.year}({card.year}){/if} -
- {/if} - - {#if card.university} -
- 📍 - {card.university}{card.location - ? `, ${card.location}` - : ""} -
- {/if} - - {#if card.awards && card.awards.length > 0} -
- {#each card.awards as award} -
- 🏆 - {award.place} — {award.track} -
- {/each} -
- {/if} - -

{card.description}

- - {#if card.tags && card.tags.length > 0} -
- {#each card.tags as tag} - {tag} - {/each} -
- {/if} - - {#if card.liveWarning} -
Demo may be unavailable
- {/if} - -
- {#if card.link} - - - Demo - - {/if} - {#if card.repo} - - - Code - - {/if} - {#if card.devpost} - - - Devpost - - {/if} -
-
-
- {/each} -
- - diff --git a/src/lib/components/tui/TuiCheckbox.svelte b/src/lib/components/tui/TuiCheckbox.svelte deleted file mode 100644 index 3a5cd74..0000000 --- a/src/lib/components/tui/TuiCheckbox.svelte +++ /dev/null @@ -1,151 +0,0 @@ - - - - - diff --git a/src/lib/components/tui/TuiFooter.svelte b/src/lib/components/tui/TuiFooter.svelte deleted file mode 100644 index 8302145..0000000 --- a/src/lib/components/tui/TuiFooter.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- - - TUI - - - {#if isTyping} - Loading... - {:else} - Ready - {/if} - - - {#if isTyping} - - {:else} - {linesCount} lines - {/if} - -
diff --git a/src/lib/components/tui/TuiGroup.svelte b/src/lib/components/tui/TuiGroup.svelte deleted file mode 100644 index f4da815..0000000 --- a/src/lib/components/tui/TuiGroup.svelte +++ /dev/null @@ -1,198 +0,0 @@ - - -
- {#each processedChildren as group} - {#if group.kind === "inline"} -
- {#each group.items as item} - - {/each} -
- {:else} - - {/if} - {/each} -
- - diff --git a/src/lib/components/tui/TuiHeader.svelte b/src/lib/components/tui/TuiHeader.svelte deleted file mode 100644 index ec3bafd..0000000 --- a/src/lib/components/tui/TuiHeader.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- - - {user.username}@{user.hostname} - - {title} - - {#if interactive && hasButtons} - ↑↓ navigate - ⏎ select - {/if} - -
diff --git a/src/lib/components/tui/TuiInput.svelte b/src/lib/components/tui/TuiInput.svelte deleted file mode 100644 index 0ae31a9..0000000 --- a/src/lib/components/tui/TuiInput.svelte +++ /dev/null @@ -1,218 +0,0 @@ - - -
- {#if line.content} - - {/if} - -
- ❯ - {#if prefix} - {prefix} - {/if} - - {#if suffix} - {suffix} - {/if} -
- - {#if hasError && errorMessage} -
- - {errorMessage} -
- {/if} -
- - diff --git a/src/lib/components/tui/TuiLine.svelte b/src/lib/components/tui/TuiLine.svelte deleted file mode 100644 index d0d1b5e..0000000 --- a/src/lib/components/tui/TuiLine.svelte +++ /dev/null @@ -1,251 +0,0 @@ - - -
-{#if isBlank} - {#if inline}{:else}
{/if} -{:else if isDivider} -
- - {#if line.content}{line.content}{/if} - -
-{:else if line.type === "button"} - -{:else if line.type === "link"} - {#if inline} - handleNavigation(line)} /> - {:else} - - {/if} -{:else if line.type === "tooltip"} - {#if inline}{:else}
- -
{/if} -{: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 === "input"} - -{:else if line.type === "textarea"} - -{:else if line.type === "checkbox"} - -{:else if line.type === "radio"} - -{:else if line.type === "select"} - -{:else if line.type === "toggle"} - -{:else if line.type === "group"} - -{:else if line.type === "image" && showImage} -
- {line.imageAlt - {#if line.content}{line.content}{/if} -
-{:else if line.type === "header"} - {#if inline} - - - {#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - - {:else} -
- - - {#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - - {#if showCursor}{/if} -
- {/if} -{:else if line.type === "command" || line.type === "prompt"} - {#if inline} - - {getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - - {:else} -
- - {user.username}@{user.hostname} - :~$ - - - {getLinePrefix( - line.type, - )}{#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - - {#if showCursor}{/if} -
- {/if} -{:else if !isImage} - {#if inline} - - {getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - {#if showCursor}{/if} - - {:else} -
- - {getLinePrefix( - line.type, - )}{#each segments as seg}{#if seg.icon}{:else if getSegmentStyle(seg)}{seg.text}{:else}{seg.text}{/if}{/each} - - {#if showCursor}{/if} -
- {/if} -{/if} -
diff --git a/src/lib/components/tui/TuiLink.svelte b/src/lib/components/tui/TuiLink.svelte deleted file mode 100644 index 6bb3b7e..0000000 --- a/src/lib/components/tui/TuiLink.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - - - {#if line.icon} - - {/if} - - {#if isExternal} - - {/if} - - - diff --git a/src/lib/components/tui/TuiProgress.svelte b/src/lib/components/tui/TuiProgress.svelte deleted file mode 100644 index f7abe3c..0000000 --- a/src/lib/components/tui/TuiProgress.svelte +++ /dev/null @@ -1,262 +0,0 @@ - - -
- {#if line.content} -
- {#each contentSegments as segment} - {#if segment.icon} - - {:else if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} -
- {/if} - - {#if line.progressStyle === "text"} -
- [ - - {#each Array(charCapacity) as _, i} - {i < Math.floor((progress / 100) * charCapacity) - ? "#" - : "."} - {/each} - - ] -
- {:else} -
-
- -
- {#if line.progressStyle !== "continuous"} -
- {#each Array(20) as _, i} - - {/each} -
- {/if} -
- {/if} - -
- {#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/TuiRadio.svelte b/src/lib/components/tui/TuiRadio.svelte deleted file mode 100644 index cfeb05e..0000000 --- a/src/lib/components/tui/TuiRadio.svelte +++ /dev/null @@ -1,206 +0,0 @@ - - -
- {#if line.content} -
- {#if line.icon} - - {/if} - {#each labelSegments as segment} - {#if segment.icon} - - {:else if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} -
- {/if} - -
- {#each options as option} - {@const isSelected = value === option.value} - {@const optionSegments = parseColorText( - option.label, - $themeColors.colorMap, - )} - - {/each} -
-
- - diff --git a/src/lib/components/tui/TuiSelect.svelte b/src/lib/components/tui/TuiSelect.svelte deleted file mode 100644 index 2e1597b..0000000 --- a/src/lib/components/tui/TuiSelect.svelte +++ /dev/null @@ -1,434 +0,0 @@ - - - - -
- {#if line.content} - - {/if} - -
- ❯ - - {displayValue || placeholder} - - {isOpen ? "▲" : "▼"} -
- - {#if isOpen} -
- {#if searchable} - - {/if} -
- {#each filteredOptions as option, i} - {@const optionSegments = parseColorText(option.label, $themeColors.colorMap)} -
- !option.disabled && handleSelect(option.value)} - onkeydown={(e) => - e.key === "Enter" && - !option.disabled && - handleSelect(option.value)} - onmouseenter={() => (highlightedIndex = i)} - > - {#if option.icon} - - {/if} - - {#each optionSegments as segment} - {#if segment.icon} - - {:else if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - - {#if value === option.value} - - {/if} -
- {:else} -
No options found
- {/each} -
-
- {/if} - - {#if hasError && errorMessage} -
- - {errorMessage} -
- {/if} -
- - diff --git a/src/lib/components/tui/TuiTable.svelte b/src/lib/components/tui/TuiTable.svelte deleted file mode 100644 index 1517f55..0000000 --- a/src/lib/components/tui/TuiTable.svelte +++ /dev/null @@ -1,112 +0,0 @@ - - -
- {#if line.content} -
{line.content}
- {/if} - - {#if headers.length > 0} - - - {#each headers as header} - - {/each} - - - {/if} - - {#each rows as row, i} - - {#each row as cell} - {@const cellSegments = parseColorText( - cell, - $themeColors.colorMap, - )} - - {/each} - - {/each} - -
{header}
- {#each cellSegments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} -
-
- - diff --git a/src/lib/components/tui/TuiTextarea.svelte b/src/lib/components/tui/TuiTextarea.svelte deleted file mode 100644 index 905f06d..0000000 --- a/src/lib/components/tui/TuiTextarea.svelte +++ /dev/null @@ -1,238 +0,0 @@ - - -
- {#if line.content} - - {/if} - -
-
- {#each Array(Math.max(rows, value.split("\n").length)) as _, i} - {i + 1} - {/each} -
- -
- - -
- - diff --git a/src/lib/components/tui/TuiToggle.svelte b/src/lib/components/tui/TuiToggle.svelte deleted file mode 100644 index f7a7a59..0000000 --- a/src/lib/components/tui/TuiToggle.svelte +++ /dev/null @@ -1,192 +0,0 @@ - - -
- {#if line.icon} - - {/if} - - {#if line.content} - - {#each labelSegments as segment} - {#if segment.icon} - - {:else if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - - {/if} - -
- {#if showLabels} - {offLabel} - {/if} - - {checked ? "●" : "○"} - - {#if showLabels} - {onLabel} - {/if} -
-
- - diff --git a/src/lib/components/tui/TuiTooltip.svelte b/src/lib/components/tui/TuiTooltip.svelte deleted file mode 100644 index 52bc8a1..0000000 --- a/src/lib/components/tui/TuiTooltip.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - - - - {#each contentSegments as segment} - {#if getSegmentStyle(segment)} - {segment.text} - {:else} - {segment.text} - {/if} - {/each} - - (?) - - -{#if showTooltip && line.tooltipText} - - {line.tooltipText} - - -{/if} - - diff --git a/src/lib/components/tui/terminal-api.ts b/src/lib/components/tui/terminal-api.ts deleted file mode 100644 index 639976c..0000000 --- a/src/lib/components/tui/terminal-api.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Terminal API Factory - * - * Creates a reactive API for manipulating terminal content programmatically. - * Use this with TerminalTUI's bindable `terminal` prop. - */ - -import type { TerminalLine, ParsedLine, DisplayedLine, TerminalAPI } from './types'; -import { parseColorText, getPlainText } from './utils'; -import type { ThemeColorMap } from './utils'; - -export interface TerminalState { - lines: TerminalLine[]; - displayedLines: DisplayedLine[]; - isTyping: boolean; - isComplete: boolean; - currentLineIndex: number; - selectedIndex: number; - skipRequested: boolean; -} - -export interface TerminalAPIOptions { - getState: () => TerminalState; - setState: (updates: Partial) => void; - getColorMap: () => ThemeColorMap; - getBodyElement: () => HTMLDivElement | undefined; - typeText: () => void; -} - -/** - * Parse a single terminal line into segments - */ -export function parseLine(line: TerminalLine, colorMap: ThemeColorMap): ParsedLine { - const segments = parseColorText(line.content, colorMap); - return { - line, - segments, - plainText: getPlainText(segments) - }; -} - -/** - * Create a terminal API instance - */ -export function createTerminalAPI(options: TerminalAPIOptions): TerminalAPI { - const { getState, setState, getColorMap, getBodyElement, typeText } = options; - - function refreshDisplayedLines() { - const state = getState(); - const colorMap = getColorMap(); - // Only update lines that are already displayed to avoid forcing untyped lines to appear - const displayedLines = state.displayedLines.map((displayed, i) => { - const line = state.lines[i]; - if (!line) return displayed; // Should not happen if state is consistent - - const parsed = parseLine(line, colorMap); - return { - parsed, - charIndex: displayed.complete ? parsed.plainText.length : displayed.charIndex, - complete: displayed.complete, - showImage: displayed.showImage ?? (line.type === 'image') - }; - }); - setState({ displayedLines, lines: [...state.lines] }); - } - - function apiScrollToBottom() { - const bodyElement = getBodyElement(); - if (!bodyElement) return; - bodyElement.scrollTo({ - top: bodyElement.scrollHeight, - behavior: 'smooth' - }); - } - - function apiScrollToLine(index: number) { - const bodyElement = getBodyElement(); - if (!bodyElement) return; - const lineElements = bodyElement.querySelectorAll('.tui-line'); - const target = lineElements[index] as HTMLElement | undefined; - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - } - - return { - clear: () => { - setState({ - lines: [], - displayedLines: [], - isComplete: true, - isTyping: false - }); - }, - - write: (line: TerminalLine) => { - const state = getState(); - const colorMap = getColorMap(); - const parsed = parseLine(line, colorMap); - setState({ - lines: [...state.lines, line], - displayedLines: [...state.displayedLines, { - parsed, - charIndex: parsed.plainText.length, - complete: true, - showImage: line.type === 'image' - }] - }); - setTimeout(apiScrollToBottom, 10); - }, - - writeLines: (newLines: TerminalLine[]) => { - const state = getState(); - const colorMap = getColorMap(); - const newDisplayed = newLines.map(line => { - const parsed = parseLine(line, colorMap); - return { - parsed, - charIndex: parsed.plainText.length, - complete: true, - showImage: line.type === 'image' - }; - }); - setState({ - lines: [...state.lines, ...newLines], - displayedLines: [...state.displayedLines, ...newDisplayed] - }); - setTimeout(apiScrollToBottom, 10); - }, - - update: (index: number, updates: Partial) => { - const state = getState(); - if (index < 0 || index >= state.lines.length) return; - // Cast to TerminalLine to allow partial updates across union types - state.lines[index] = { ...state.lines[index], ...updates } as TerminalLine; - refreshDisplayedLines(); - }, - - updateContent: (index: number, content: string) => { - const state = getState(); - if (index < 0 || index >= state.lines.length) return; - state.lines[index] = { ...state.lines[index], content }; - refreshDisplayedLines(); - }, - - insert: (index: number, line: TerminalLine) => { - const state = getState(); - const clampedIndex = Math.max(0, Math.min(index, state.lines.length)); - setState({ - lines: [...state.lines.slice(0, clampedIndex), line, ...state.lines.slice(clampedIndex)] - }); - refreshDisplayedLines(); - }, - - remove: (index: number) => { - const state = getState(); - if (index < 0 || index >= state.lines.length) return; - setState({ - lines: [...state.lines.slice(0, index), ...state.lines.slice(index + 1)] - }); - refreshDisplayedLines(); - }, - - removeRange: (startIndex: number, count: number) => { - const state = getState(); - if (startIndex < 0 || startIndex >= state.lines.length) return; - setState({ - lines: [...state.lines.slice(0, startIndex), ...state.lines.slice(startIndex + count)] - }); - refreshDisplayedLines(); - }, - - setLines: (newLines: TerminalLine[]) => { - setState({ lines: [...newLines] }); - refreshDisplayedLines(); - }, - - getLineCount: () => getState().lines.length, - - getLine: (index: number) => getState().lines[index], - - getLines: () => [...getState().lines], - - findById: (id: string) => getState().lines.findIndex(line => line.id === id), - - updateById: (id: string, updates: Partial) => { - const state = getState(); - const index = state.lines.findIndex(line => line.id === id); - if (index !== -1) { - // Cast to TerminalLine to allow partial updates across union types - state.lines[index] = { ...state.lines[index], ...updates } as TerminalLine; - refreshDisplayedLines(); - } - }, - - removeById: (id: string) => { - const state = getState(); - const index = state.lines.findIndex(line => line.id === id); - if (index !== -1) { - setState({ - lines: [...state.lines.slice(0, index), ...state.lines.slice(index + 1)] - }); - refreshDisplayedLines(); - } - }, - - scrollToBottom: apiScrollToBottom, - - scrollToLine: apiScrollToLine, - - isAnimating: () => getState().isTyping, - - skip: () => { - const state = getState(); - if (!state.isTyping) return; - - const colorMap = getColorMap(); - const displayedLines = state.lines.map(line => { - const parsed = parseLine(line, colorMap); - return { - parsed, - charIndex: parsed.plainText.length, - complete: true, - showImage: line.type === 'image' - }; - }); - - setState({ - skipRequested: true, - displayedLines, - isTyping: false, - isComplete: true, - currentLineIndex: state.lines.length - 1 - }); - - apiScrollToBottom(); - }, - - restart: () => { - setState({ - displayedLines: [], - currentLineIndex: 0, - isTyping: false, - isComplete: false, - skipRequested: false, - selectedIndex: -1 - }); - typeText(); - } - }; -} diff --git a/src/lib/components/tui/terminal-commands.ts b/src/lib/components/tui/terminal-commands.ts deleted file mode 100644 index fa261f0..0000000 --- a/src/lib/components/tui/terminal-commands.ts +++ /dev/null @@ -1,405 +0,0 @@ -import type { TerminalAPI, TerminalLine } from './types'; -import { user } from '$lib/config'; - -export interface CommandContext { - terminal: TerminalAPI; - navigate: (path: string, external?: boolean) => void; - toggleMode: () => void; - setColorTheme: (name: string) => void; - getMode: () => string; - getColorTheme: () => string; - themeNames: string[]; - user: typeof user; - skills: Record; - navigation: Array<{ name: string; path: string; external?: boolean }>; -} - -export interface Command { - name: string; - description: string; - usage?: string; - aliases?: string[]; - hidden?: boolean; - run: (args: string[], ctx: CommandContext) => void; -} - -const out = (content: string): TerminalLine => ({ type: 'output', content }); -const ok = (content: string): TerminalLine => ({ type: 'success', content }); -const err = (content: string): TerminalLine => ({ type: 'error', content }); -const info = (content: string): TerminalLine => ({ type: 'info', content }); -const blank = (): TerminalLine => ({ type: 'blank', content: '' }); - -interface PageNode { - name: string; - path: string; - children?: PageNode[]; -} - -const pageTree: PageNode[] = [ - { name: 'home', path: '/' }, - { name: 'about', path: '/about' }, - { name: 'portfolio', path: '/portfolio' }, - { - name: 'projects', - path: '/projects', - children: [ - { name: 'opensource', path: '/projects/opensource' }, - { name: 'packages', path: '/projects/packages' }, - { name: 'hackathons', path: '/projects/hackathons' }, - { name: 'research', path: '/projects/research' } - ] - }, - { name: 'models', path: '/models' }, - { name: 'components', path: '/components' }, - { name: 'links', path: '/links' }, - { name: 'blog', path: '/blog' } -]; - -const pageRoutes: Record = pageTree.reduce((routes, node) => { - routes[node.name] = node.path; - node.children?.forEach((child) => { - routes[child.name] = child.path; - }); - return routes; -}, {} as Record); - -function resolvePagePath(target: string): string | undefined { - if (pageRoutes[target] !== undefined) return pageRoutes[target]; - - const segments = target.split('/').filter(Boolean); - if (segments.length < 2) return undefined; - - let nodes: PageNode[] = pageTree; - let path: string | undefined; - for (const segment of segments) { - const node = nodes.find((candidate) => candidate.name === segment); - if (!node) return undefined; - path = node.path; - nodes = node.children ?? []; - } - return path; -} - -function cowsay(message: string): string[] { - const text = message || 'Moo?'; - const border = '-'.repeat(text.length + 2); - return [ - ` ${border}`, - `< ${text} >`, - ` ${border}`, - ' \\ ^__^', - ' \\ (oo)\\_______', - ' (__)\\ )\\/\\', - ' ||----w |', - ' || ||' - ]; -} - -const baseCommands: Command[] = [ - { - name: 'help', - aliases: ['h', '?'], - description: 'List all available commands', - run: (_args, ctx) => { - const rows = commands - .filter((command) => !command.hidden) - .map((command) => [ - `(&primary,bold)${command.name}(&)` + - (command.aliases?.length ? ` (&muted)(${command.aliases.join(', ')})(&)` : ''), - command.description - ]); - ctx.terminal.write({ - type: 'table', - content: 'Available Commands', - tableHeaders: ['Command', 'Description'], - tableRows: rows, - style: 'accent' - } as TerminalLine); - ctx.terminal.write(info('Tip: try (&accent)neofetch(&), (&accent)cowsay(&), or (&accent)cd projects(&)')); - ctx.terminal.write(blank()); - } - }, - { - name: 'clear', - aliases: ['cls'], - description: 'Clear the terminal', - run: (_args, ctx) => ctx.terminal.clear() - }, - { - name: 'whoami', - description: 'Show a short bio', - run: (_args, ctx) => { - ctx.terminal.writeLines([ - out(`(&primary,bold)${ctx.user.displayname}(&) (&muted)— ${ctx.user.title}(&)`), - out(`(&muted)${ctx.user.bio}(&)`), - blank() - ]); - } - }, - { - name: 'neofetch', - aliases: ['banner'], - description: 'Display system info', - run: (_args, ctx) => { - const launched = new Date('2020-01-01T00:00:00Z'); - const days = Math.floor((Date.now() - launched.getTime()) / 86400000); - const infoLines: TerminalLine[] = [ - out(`(&accent,bold)${ctx.user.username}(&)(&muted)@(&)(&primary,bold)${ctx.user.hostname}(&)`), - out('(&muted)------------------------------(&)'), - out(`(&blue,bold)Name(&): ${ctx.user.displayname}`), - out(`(&blue,bold)Title(&): ${ctx.user.title}`), - out(`(&blue,bold)Location(&): ${ctx.user.location}`), - out(`(&blue,bold)Shell(&): tui-terminal`), - out(`(&blue,bold)Theme(&): ${ctx.getColorTheme()} (${ctx.getMode()})`), - out(`(&blue,bold)Uptime(&): ${days} days since launch`), - out(`(&blue,bold)Languages(&): ${ctx.skills.languages.slice(0, 6).join(', ')}`), - out('(&red)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)') - ]; - ctx.terminal.writeLines([ - { - type: 'image', - content: '', - image: ctx.user.avatar, - imageAlt: ctx.user.displayname, - imageWidth: 325, - inline: true - }, - { - type: 'group', - content: '', - inline: true, - groupDirection: 'column', - groupGap: '0', - children: infoLines - }, - blank() - ]); - } - }, - { - name: 'skills', - description: 'List skills by category', - run: (_args, ctx) => { - const order = ['languages', 'frameworks', 'tools', 'platforms', 'applications', 'databases', 'interests']; - const lines: TerminalLine[] = []; - for (const key of order) { - const values = ctx.skills[key]; - if (!values?.length) continue; - lines.push(info(`(&blue,bold)${key}(&)`)); - lines.push(out(' ' + values.map((value) => `(&cyan)${value}(&)`).join(' (&muted)•(&) '))); - } - lines.push(blank()); - ctx.terminal.writeLines(lines); - } - }, - { - name: 'contact', - aliases: ['socials'], - description: 'Show social links', - run: (_args, ctx) => { - ctx.terminal.write(info('(&accent,bold)Find me online:(&)')); - ctx.terminal.writeLines( - ctx.user.socials.map((social) => ({ - type: 'button', - content: social.name, - icon: social.icon, - href: social.link, - style: 'primary', - inline: true - })) as TerminalLine[] - ); - ctx.terminal.write(blank()); - } - }, - { - name: 'email', - description: 'Show contact email', - run: (_args, ctx) => { - ctx.terminal.write(out(`(&accent)${ctx.user.email}(&)`)); - } - }, - { - name: 'cd', - aliases: ['goto', 'open'], - description: 'Navigate to a page', - usage: 'cd (e.g. projects/opensource)', - run: (args, ctx) => { - const target = (args[0] || 'home').toLowerCase().replace(/^\/+|\/+$/g, ''); - const navItem = ctx.navigation.find((item) => item.name.toLowerCase() === target); - if (navItem) { - ctx.terminal.write(info(`Navigating to (&accent)${target}(&)...`)); - ctx.navigate(navItem.path, navItem.external); - return; - } - const resolvedPath = resolvePagePath(target); - if (resolvedPath !== undefined) { - ctx.terminal.write(info(`Navigating to (&accent)${target}(&)...`)); - ctx.navigate(resolvedPath); - return; - } - ctx.terminal.write(err(`No such page: ${target}. Try (&bold)ls(&).`)); - } - }, - { - name: 'ls', - aliases: ['dir'], - description: 'List the pages you can visit', - run: (_args, ctx) => { - const lines: TerminalLine[] = []; - - pageTree.forEach((node) => { - lines.push(out(`(&blue)drwxr-xr-x(&) (&primary,bold)${node.name}(&)/`)); - node.children?.forEach((child, i) => { - const branch = i === node.children!.length - 1 ? '└──' : '├──'; - lines.push(out(`(&muted) ${branch}(&) (&primary,bold)${child.name}(&)/`)); - }); - }); - - ctx.navigation.forEach((item) => { - if (item.external && pageRoutes[item.name] === undefined) { - lines.push(out(`(&blue)drwxr-xr-x(&) (&primary,bold)${item.name}(&)/`)); - } - }); - - ctx.terminal.writeLines(lines); - } - }, - { - name: 'theme', - description: 'List or set the color theme', - usage: 'theme [name]', - run: (args, ctx) => { - if (!args.length) { - ctx.terminal.write(out('Themes: ' + ctx.themeNames.map((name) => `(&blue)${name}(&)`).join(' '))); - ctx.terminal.write(info(`Current: (&accent)${ctx.getColorTheme()}(&)`)); - return; - } - const name = args[0].toLowerCase(); - if (ctx.themeNames.includes(name)) { - ctx.setColorTheme(name); - ctx.terminal.write(ok(`Theme set to (&accent)${name}(&)`)); - } else { - ctx.terminal.write(err(`Unknown theme: ${name}. Available: ${ctx.themeNames.join(', ')}`)); - } - } - }, - { - name: 'mode', - description: 'Toggle or set dark/light mode', - usage: 'mode [dark|light]', - run: (args, ctx) => { - const current = ctx.getMode(); - if (!args.length) { - ctx.toggleMode(); - ctx.terminal.write(ok(`Switched to ${current === 'dark' ? 'light' : 'dark'} mode`)); - return; - } - const requested = args[0].toLowerCase(); - if (requested === 'dark' || requested === 'light') { - if (requested !== current) ctx.toggleMode(); - ctx.terminal.write(ok(`${requested} mode active`)); - } else { - ctx.terminal.write(err('Usage: mode [dark|light]')); - } - } - }, - { - name: 'echo', - description: 'Print text back', - usage: 'echo ', - run: (args, ctx) => ctx.terminal.write(out(args.join(' '))) - }, - { - name: 'cowsay', - description: 'Make a cow say something', - usage: 'cowsay ', - run: (args, ctx) => { - const lines = cowsay(args.join(' ')); - ctx.terminal.writeLines(lines.map((line) => out(`(&accent)${line}(&)`))); - } - }, - { - name: 'date', - description: 'Show the current date and time (Eastern)', - run: (_args, ctx) => { - const now = new Date(); - const easternString = now.toLocaleString('en-US', { - timeZone: 'America/New_York', - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short' - }); - - const easternParts = new Intl.DateTimeFormat('en-US', { - timeZone: 'America/New_York', - year: 'numeric', - month: '2-digit', - day: '2-digit' - }).formatToParts(now); - const part = (type: string) => Number(easternParts.find((p) => p.type === type)?.value); - const easternDate = new Date(Date.UTC(part('year'), part('month') - 1, part('day'))); - const startOfYear = new Date(Date.UTC(part('year'), 0, 1)); - const dayOfYear = Math.floor((easternDate.getTime() - startOfYear.getTime()) / 86400000) + 1; - - ctx.terminal.writeLines([ - out(easternString), - info(`(&muted)Day ${dayOfYear} of ${part('year')}(&)`) - ]); - } - }, - { - name: 'sudo', - hidden: true, - description: 'Elevated privileges', - run: (_args, ctx) => - ctx.terminal.write( - err(`(&error)Nice try.(&) ${ctx.user.username} is not in the sudoers file. This incident will be reported.`) - ) - } -]; - -const pageCommands: Command[] = Object.entries(pageRoutes).map(([name, path]) => ({ - name, - description: `Go to the ${name} page`, - hidden: true, - run: (_args, ctx) => ctx.navigate(path) -})); - -const socialCommands: Command[] = user.socials.map((social) => ({ - name: social.name.toLowerCase(), - description: `Open ${social.name}`, - hidden: true, - run: (_args, ctx) => ctx.navigate(social.link, true) -})); - -export const commands: Command[] = [...baseCommands, ...pageCommands, ...socialCommands]; - -function findCommand(name: string): Command | undefined { - const lower = name.toLowerCase(); - return commands.find( - (command) => command.name === lower || command.aliases?.some((alias) => alias.toLowerCase() === lower) - ); -} - -export function runCommand(input: string, ctx: CommandContext): void { - const raw = input.trim(); - ctx.terminal.write({ type: 'command', content: raw }); - if (!raw) return; - - const parts = raw.split(/\s+/); - const name = parts[0]; - const args = parts.slice(1); - const command = findCommand(name); - - if (!command) { - ctx.terminal.write(err(`command not found: (&bold)${name}(&) — type (&accent)help(&) for a list.`)); - return; - } - - command.run(args, ctx); -} diff --git a/src/lib/components/tui/terminal-keyboard.ts b/src/lib/components/tui/terminal-keyboard.ts deleted file mode 100644 index 823f2b3..0000000 --- a/src/lib/components/tui/terminal-keyboard.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Terminal Keyboard Handler - * - * Handles keyboard navigation and interaction for the terminal. - */ - -import type { DisplayedLine } from './types'; -import { browser } from '$app/environment'; -import { goto } from '$app/navigation'; - -export interface KeyboardHandlerOptions { - getIsTyping: () => boolean; - getIsComplete: () => boolean; - getInteractive: () => boolean; - getButtonIndices: () => number[]; - getSelectedIndex: () => number; - setSelectedIndex: (index: number) => void; - getDisplayedLines: () => DisplayedLine[]; - getBodyElement: () => HTMLDivElement | undefined; - skipAnimation: () => void; - scrollMargin?: number; -} - -/** - * Scroll selected button into view with margin for context - */ -export function scrollToSelected( - bodyElement: HTMLDivElement | undefined, - selectedIndex: number, - displayedLines: DisplayedLine[], - scrollMargin: number = 80 -): void { - if (!bodyElement || selectedIndex < 0) return; - - // Get all buttons in DOM order (including nested ones in groups) - const allButtons = bodyElement.querySelectorAll('.tui-button'); - const selectedButton = allButtons[selectedIndex] as HTMLElement | undefined; - - if (selectedButton && bodyElement) { - const containerRect = bodyElement.getBoundingClientRect(); - const buttonRect = selectedButton.getBoundingClientRect(); - - if (buttonRect.top < containerRect.top + scrollMargin) { - const scrollAmount = buttonRect.top - containerRect.top - scrollMargin; - bodyElement.scrollBy({ top: scrollAmount, behavior: 'smooth' }); - } else if (buttonRect.bottom > containerRect.bottom - scrollMargin) { - const scrollAmount = buttonRect.bottom - containerRect.bottom + scrollMargin; - bodyElement.scrollBy({ top: scrollAmount, behavior: 'smooth' }); - } - } -} - -/** - * Handle button/link click navigation - */ -export function handleNavigation(line: { action?: () => void; href?: string; external?: boolean } | undefined): void { - if (!line) return; - - if (line.action) { - line.action(); - } else if (line.href) { - const isExternal = line.external || line.href.startsWith('http://') || line.href.startsWith('https://'); - if (isExternal) { - window.open(line.href, '_blank', 'noopener,noreferrer'); - } else { - // Use SvelteKit's goto for client-side navigation (no page reload) - goto(line.href); - } - } -} - -/** - * Create a keyboard event handler for the terminal - */ -export function createKeyboardHandler(options: KeyboardHandlerOptions) { - const { - getIsTyping, - getIsComplete, - getInteractive, - getButtonIndices, - getSelectedIndex, - setSelectedIndex, - getDisplayedLines, - getBodyElement, - skipAnimation, - scrollMargin = 80 - } = options; - - return function handleKeydown(event: KeyboardEvent): void { - // Ignore key events originating from editable fields (e.g. command input) - const target = event.target as HTMLElement | null; - if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) { - return; - } - - // Skip animation on Y key - if (getIsTyping() && (event.key === 'y' || event.key === 'Y')) { - event.preventDefault(); - skipAnimation(); - return; - } - - if (!getInteractive() || !getIsComplete()) return; - - const bodyElement = getBodyElement(); - if (!bodyElement) return; - - // Get all buttons from the DOM (including nested ones in groups) - const allButtons = bodyElement.querySelectorAll('.tui-button'); - const buttonCount = allButtons.length; - if (buttonCount === 0) return; - - const selectedIndex = getSelectedIndex(); - const displayedLines = getDisplayedLines(); - - if (event.key === 'ArrowDown' || event.key === 'j') { - event.preventDefault(); - const nextIdx = selectedIndex < 0 ? 0 : (selectedIndex + 1) % buttonCount; - setSelectedIndex(nextIdx); - scrollToSelected(bodyElement, nextIdx, displayedLines, scrollMargin); - // Update visual selection on buttons - allButtons.forEach((btn, i) => btn.classList.toggle('selected', i === nextIdx)); - } else if (event.key === 'ArrowUp' || event.key === 'k') { - event.preventDefault(); - const prevIdx = selectedIndex < 0 ? buttonCount - 1 : (selectedIndex - 1 + buttonCount) % buttonCount; - setSelectedIndex(prevIdx); - scrollToSelected(bodyElement, prevIdx, displayedLines, scrollMargin); - // Update visual selection on buttons - allButtons.forEach((btn, i) => btn.classList.toggle('selected', i === prevIdx)); - } else if (event.key === 'Enter') { - event.preventDefault(); - if (selectedIndex >= 0 && selectedIndex < buttonCount) { - const selectedButton = allButtons[selectedIndex] as HTMLElement; - if (selectedButton) { - // Read navigation data from button data attributes - const href = selectedButton.dataset.href; - const isExternal = selectedButton.dataset.external === 'true'; - - if (href) { - handleNavigation({ href, external: isExternal }); - } else { - // Fallback to click for buttons with actions (non-navigational) - selectedButton.click(); - } - } - } - } - }; -} - -/** - * Scroll to hash target (anchor link like #skills) - */ -export function scrollToHash(bodyElement: HTMLDivElement | undefined): void { - if (!browser || !bodyElement) return; - - const hash = window.location.hash; - if (!hash) return; - - const targetId = hash.slice(1); // Remove the # - const targetElement = bodyElement.querySelector(`#${CSS.escape(targetId)}`); - - if (targetElement) { - // Small delay to ensure layout is complete - setTimeout(() => { - targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, 100); - } -} diff --git a/src/lib/components/tui/terminal-typing.ts b/src/lib/components/tui/terminal-typing.ts deleted file mode 100644 index 26163f5..0000000 --- a/src/lib/components/tui/terminal-typing.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Terminal Typing Animation Engine - * - * Handles the character-by-character typing animation for terminal lines. - */ - -import type { ParsedLine, DisplayedLine } from './types'; -import type { TerminalSettings } from '$lib/config'; -import { calculateTypeSpeed } from '$lib'; - -export interface TypingState { - displayedLines: DisplayedLine[]; - currentLineIndex: number; - isTyping: boolean; - isComplete: boolean; - skipRequested: boolean; - selectedIndex: number; -} - -export interface TypingOptions { - parsedLines: ParsedLine[]; - speedMultiplier: number; - terminalSettings: TerminalSettings; - buttonIndices: number[]; - interactive: boolean; - autoscroll: boolean; - getBodyElement: () => HTMLDivElement | undefined; - getState: () => TypingState; - setState: (updates: Partial) => void; - onComplete?: () => void; - scrollToHash: () => void; -} - -function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Scroll terminal body to bottom - */ -function scrollToBottom(bodyElement: HTMLDivElement | undefined, autoscroll: boolean) { - if (!autoscroll || !bodyElement) return; - bodyElement.scrollTo({ - top: bodyElement.scrollHeight, - behavior: 'smooth' - }); -} - -/** - * Run the typing animation for all lines - */ -export async function runTypingAnimation(options: TypingOptions): Promise { - const { - parsedLines, - speedMultiplier, - terminalSettings, - buttonIndices, - interactive, - autoscroll, - getBodyElement, - getState, - setState, - onComplete, - scrollToHash - } = options; - - if (parsedLines.length === 0) return; - - setState({ isTyping: true }); - - // Apply speed multiplier to start delay - const startDelayMs = speedMultiplier === 0 ? 0 : terminalSettings.startDelay * speedMultiplier; - await sleep(startDelayMs); - if (getState().skipRequested) return; - - for (let i = 0; i < parsedLines.length; i++) { - const state = getState(); - if (state.skipRequested) return; - - const parsed = parsedLines[i]; - const line = parsed.line; - const plainLength = parsed.plainText.length; - - // Add new line to displayed lines - const currentDisplayed = [...state.displayedLines, { parsed, charIndex: 0, complete: false, showImage: false }]; - setState({ displayedLines: currentDisplayed, currentLineIndex: i }); - - if (line.delay && speedMultiplier > 0) { - await sleep(line.delay * speedMultiplier); - if (getState().skipRequested) return; - } - - const bodyElement = getBodyElement(); - - // Handle different line types - if (line.type === 'image') { - await sleep(speedMultiplier === 0 ? 0 : 100 * speedMultiplier); - if (getState().skipRequested) return; - updateDisplayedLine(i, { parsed, charIndex: plainLength, complete: true, showImage: true }, getState, setState); - scrollToBottom(bodyElement, autoscroll); - } else if (line.type === 'blank' || line.type === 'divider') { - updateDisplayedLine(i, { parsed, charIndex: plainLength, complete: true, showImage: false }, getState, setState); - } else if (line.type === 'button') { - // Buttons appear instantly - updateDisplayedLine(i, { parsed, charIndex: plainLength, complete: true, showImage: false }, getState, setState); - scrollToBottom(bodyElement, autoscroll); - } else if (speedMultiplier === 0) { - // Instant mode - no typing animation - updateDisplayedLine(i, { parsed, charIndex: plainLength, complete: true, showImage: false }, getState, setState); - if (i % 5 === 0) scrollToBottom(bodyElement, autoscroll); - } else if (line.type === 'header') { - const typeSpeed = calculateTypeSpeed(plainLength, speedMultiplier * 0.25); - for (let j = 0; j <= plainLength; j++) { - if (getState().skipRequested) return; - updateDisplayedLine(i, { - parsed, - charIndex: j, - complete: j === plainLength, - showImage: false - }, getState, setState); - if (j < plainLength) await sleep(typeSpeed); - } - scrollToBottom(bodyElement, autoscroll); - } else { - const typeSpeed = calculateTypeSpeed(plainLength, speedMultiplier); - - for (let j = 0; j <= plainLength; j++) { - if (getState().skipRequested) return; - updateDisplayedLine(i, { - parsed, - charIndex: j, - complete: j === plainLength, - showImage: false - }, getState, setState); - - if (j % 10 === 0) scrollToBottom(bodyElement, autoscroll); - - if (j < plainLength) { - await sleep(typeSpeed); - } - } - } - - if (getState().skipRequested) return; - - // Mark line as complete - const finalState = getState(); - const updatedLines = [...finalState.displayedLines]; - if (updatedLines[i]) { - updatedLines[i].complete = true; - setState({ displayedLines: updatedLines }); - } - - scrollToBottom(bodyElement, autoscroll); - - if (i < parsedLines.length - 1 && speedMultiplier > 0) { - await sleep(terminalSettings.lineDelay * speedMultiplier); - } - } - - setState({ isTyping: false, isComplete: true }); - - if (interactive && buttonIndices.length > 0) { - setState({ selectedIndex: buttonIndices[0] }); - } - - // Scroll to hash anchor if present in URL - scrollToHash(); - - onComplete?.(); -} - -/** - * Update a single displayed line at index - */ -function updateDisplayedLine( - index: number, - update: DisplayedLine, - getState: () => TypingState, - setState: (updates: Partial) => void -) { - const state = getState(); - const updatedLines = [...state.displayedLines]; - updatedLines[index] = update; - setState({ displayedLines: updatedLines }); -} - -/** - * Skip animation and show all content instantly - */ -export function skipTypingAnimation( - parsedLines: ParsedLine[], - buttonIndices: number[], - interactive: boolean, - getState: () => TypingState, - setState: (updates: Partial) => void, - getBodyElement: () => HTMLDivElement | undefined, - autoscroll: boolean, - onComplete?: () => void -): void { - const state = getState(); - if (!state.isTyping) return; - - const displayedLines = parsedLines.map(parsed => ({ - parsed, - charIndex: parsed.plainText.length, - complete: true, - showImage: parsed.line.type === 'image' - })); - - setState({ - skipRequested: true, - displayedLines, - isTyping: false, - isComplete: true, - currentLineIndex: parsedLines.length - 1, - selectedIndex: interactive && buttonIndices.length > 0 ? buttonIndices[0] : -1 - }); - - scrollToBottom(getBodyElement(), autoscroll); - onComplete?.(); -} diff --git a/src/lib/components/tui/types.ts b/src/lib/components/tui/types.ts deleted file mode 100644 index 5506089..0000000 --- a/src/lib/components/tui/types.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { TextSegment } from './utils'; -import type { Card } from '$lib/config'; - -// Re-export TextSegment for convenience -export type { TextSegment }; - -export type LineType = - | 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning' - | 'image' | 'blank' | 'header' | 'button' | 'divider' | 'link' - | 'card' | 'progress' | 'accordion' | 'table' | 'tooltip' | 'cardgrid' - | 'input' | 'textarea' | 'checkbox' | 'radio' | 'select' | 'toggle' - | 'group'; - -// Option type for radio and select components -export interface FormOption { - value: string; - label: string; - icon?: string; - disabled?: boolean; -} - -// Base interface for all terminal lines -export interface BaseTerminalLine { - type: LineType; - content: string; - id?: string; - inline?: boolean; - display?: 'flex' | 'block' | 'grid' | 'inline'; - delay?: number; - style?: 'primary' | 'secondary' | 'accent' | 'warning' | 'error'; - mobile?: boolean; -} - -// Specific line types -export interface CommandLine extends BaseTerminalLine { - type: 'command'; -} - -export interface OutputLine extends BaseTerminalLine { - type: 'output'; -} - -export interface PromptLine extends BaseTerminalLine { - type: 'prompt'; -} - -export interface ErrorLine extends BaseTerminalLine { - type: 'error'; -} - -export interface SuccessLine extends BaseTerminalLine { - type: 'success'; -} - -export interface InfoLine extends BaseTerminalLine { - type: 'info'; -} - -export interface WarningLine extends BaseTerminalLine { - type: 'warning'; -} - -export interface BlankLine extends BaseTerminalLine { - type: 'blank'; - height?: string | number; -} - -export interface DividerLine extends BaseTerminalLine { - type: 'divider'; -} - -export interface HeaderLine extends BaseTerminalLine { - type: 'header'; -} - -export interface ButtonLine extends BaseTerminalLine { - type: 'button'; - action?: () => void; - href?: string; - icon?: string; - external?: boolean; - textStyle?: 'start' | 'center' | 'end'; - border?: boolean; - flex?: boolean; -} - -export interface LinkLine extends BaseTerminalLine { - type: 'link'; - href: string; - external?: boolean; - icon?: string; -} - -export interface ImageLine extends BaseTerminalLine { - type: 'image'; - image: string; - imageAlt?: string; - imageWidth?: number; -} - -export interface CardLine extends BaseTerminalLine { - type: 'card'; - cardTitle?: string; - cardFooter?: string; - icon?: string; - image?: string; - imageAlt?: string; - children?: TerminalLine[]; - cardWidth?: string | number; - cardHeight?: string | number; - cardFloat?: 'start' | 'center' | 'end'; - link?: string; -} - -export interface ProgressLine extends BaseTerminalLine { - type: 'progress'; - progress: number; // 0-100 - progressLabel?: string; - progressStyle?: 'block' | 'continuous' | 'text'; -} - -export interface AccordionLine extends BaseTerminalLine { - type: 'accordion'; - accordionItems?: { title: string; content: string }[]; - children?: TerminalLine[]; - accordionOpen?: boolean; -} - -export interface TableLine extends BaseTerminalLine { - type: 'table'; - tableHeaders: string[]; - tableRows: string[][]; -} - -export interface TooltipLine extends BaseTerminalLine { - type: 'tooltip'; - tooltipText: string; - tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; -} - -export interface CardGridLine extends BaseTerminalLine { - type: 'cardgrid'; - cards: Card[]; -} - -export interface InputLine extends BaseTerminalLine { - type: 'input'; - inputPlaceholder?: string; - inputType?: 'text' | 'email' | 'password' | 'number' | 'url' | 'tel' | 'search'; - inputDisabled?: boolean; - inputError?: boolean; - inputErrorMessage?: string; - inputPrefix?: string; - inputSuffix?: string; - icon?: string; -} - -export interface TextareaLine extends BaseTerminalLine { - type: 'textarea'; - inputPlaceholder?: string; - textareaRows?: number; - textareaMaxLength?: number; - inputDisabled?: boolean; - inputError?: boolean; - inputErrorMessage?: string; - icon?: string; -} - -export interface CheckboxLine extends BaseTerminalLine { - type: 'checkbox'; - checkboxIndeterminate?: boolean; - inputDisabled?: boolean; - icon?: string; -} - -export interface RadioLine extends BaseTerminalLine { - type: 'radio'; - radioOptions: FormOption[]; - radioHorizontal?: boolean; - inputDisabled?: boolean; - icon?: string; -} - -export interface SelectLine extends BaseTerminalLine { - type: 'select'; - selectOptions: FormOption[]; - selectSearchable?: boolean; - inputPlaceholder?: string; - inputDisabled?: boolean; - inputError?: boolean; - inputErrorMessage?: string; - icon?: string; -} - -export interface ToggleLine extends BaseTerminalLine { - type: 'toggle'; - toggleOnLabel?: string; - toggleOffLabel?: string; - toggleShowLabels?: boolean; - inputDisabled?: boolean; - icon?: string; -} - -export interface GroupLine extends BaseTerminalLine { - type: 'group'; - children: TerminalLine[]; - groupDirection?: 'row' | 'column'; - groupAlign?: 'start' | 'center' | 'end' | 'stretch'; - groupGap?: string; - groupExpand?: boolean; -} - -// Discriminated union of all line types -export type TerminalLine = - | CommandLine | OutputLine | PromptLine | ErrorLine | SuccessLine | InfoLine | WarningLine - | BlankLine | DividerLine | HeaderLine | ButtonLine | LinkLine | ImageLine - | CardLine | ProgressLine | AccordionLine | TableLine | TooltipLine | CardGridLine - | InputLine | TextareaLine | CheckboxLine | RadioLine | SelectLine | ToggleLine - | GroupLine; - -// Terminal API for reactive manipulation -export interface TerminalAPI { - /** Clear all lines from the terminal */ - clear: () => void; - /** Write a new line to the terminal (appends) */ - write: (line: TerminalLine) => void; - /** Write multiple lines to the terminal (appends) */ - writeLines: (lines: TerminalLine[]) => void; - /** Update a specific line by index */ - update: (index: number, line: Partial) => void; - /** Update a line's content by index */ - updateContent: (index: number, content: string) => void; - /** Insert a line at a specific index */ - insert: (index: number, line: TerminalLine) => void; - /** Remove a line by index */ - remove: (index: number) => void; - /** Remove lines by range */ - removeRange: (startIndex: number, count: number) => void; - /** Replace all lines */ - setLines: (lines: TerminalLine[]) => void; - /** Get current line count */ - getLineCount: () => number; - /** Get a line by index */ - getLine: (index: number) => TerminalLine | undefined; - /** Get all lines */ - getLines: () => TerminalLine[]; - /** Find line index by id */ - findById: (id: string) => number; - /** Update a line by its id */ - updateById: (id: string, line: Partial) => void; - /** Remove a line by its id */ - removeById: (id: string) => void; - /** Scroll to bottom */ - scrollToBottom: () => void; - /** Scroll to a specific line index */ - scrollToLine: (index: number) => void; - /** Check if terminal is currently animating */ - isAnimating: () => boolean; - /** Skip current animation */ - skip: () => void; - /** Restart the typing animation */ - restart: () => void; -} - -// Pre-parsed line with segments ready for rendering -export interface ParsedLine { - line: TerminalLine; - segments: TextSegment[]; - plainText: string; // Plain text without color codes for length calculation -} - -// Display state for a line during typing animation -export interface DisplayedLine { - parsed: ParsedLine; - charIndex: number; // How many plain-text characters to show - complete: boolean; - showImage: boolean; -} diff --git a/src/lib/components/tui/utils.ts b/src/lib/components/tui/utils.ts deleted file mode 100644 index 4e6f61c..0000000 --- a/src/lib/components/tui/utils.ts +++ /dev/null @@ -1,239 +0,0 @@ -// Shared utilities used by TUI components - -export interface TextSegment { - text: string; - color?: string; - background?: string; - bold?: boolean; - dim?: boolean; - italic?: boolean; - underline?: boolean; - strikethrough?: boolean; - overline?: boolean; - // For inline icons - icon?: string; - iconSize?: number; - // For inline clickable text - href?: string; - action?: () => void; -} - -// Color maps for each theme -export type ThemeColorMap = Record; - -// Default color map (fallback) - hex values as defaults, overwritten by theme colorMap at runtime -export const defaultColorMap: ThemeColorMap = { - // Basic colors - 'red': '#f38ba8', - 'green': '#a6e3a1', - 'yellow': '#f9e2af', - 'blue': '#89b4fa', - 'magenta': '#cba6f7', - 'cyan': '#94e2d5', - 'white': '#cdd6f4', - 'gray': '#6c7086', - 'orange': '#fab387', - 'pink': '#f5c2e7', - 'black': '#1e1e2e', - 'surface': '#313244', - // Extended colors - 'teal': '#94e2d5', - 'sky': '#89dceb', - 'sapphire': '#74c7ec', - 'lavender': '#b4befe', - 'peach': '#fab387', - 'maroon': '#eba0ac', - 'mauve': '#cba6f7', - 'flamingo': '#f2cdcd', - 'rosewater': '#f5e0dc', - // Semantic colors - 'primary': '#cba6f7', - 'accent': '#a6e3a1', - 'muted': '#6c7086', - 'error': '#f38ba8', - 'success': '#a6e3a1', - 'warning': '#f9e2af', - 'info': '#89b4fa', - // Theme colors - 'text': '#cdd6f4', - 'textMuted': '#a6adc8', - 'background': '#1e1e2e', - 'backgroundLight': '#313244', - 'border': '#45475a', - 'terminal': '#1e1e2e', - 'terminalPrompt': '#cba6f7', - 'terminalUser': '#a6e3a1', - 'terminalPath': '#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, colors: ThemeColorMap = colorMap): TextSegment[] { - const segments: TextSegment[] = []; - // Match (&icon, iconName) patterns FIRST, then (&specs)content(&) - // This prevents (&icon, ...) from being consumed as the start of a color block - const regex = /\(&icon,\s*([^)]+)\)|\(&([^)]+)\)(.*?)\(&\)/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - segments.push({ text: text.slice(lastIndex, match.index) }); - } - - // Check if this is an icon match (Group 1 is the icon name, optionally followed by a size) - if (match[1]) { - const [iconName, sizeArg] = match[1].split(',').map(part => part.trim()); - const iconSize = sizeArg ? Number(sizeArg) : undefined; - segments.push({ text: '', icon: iconName, iconSize: iconSize && !isNaN(iconSize) ? iconSize : undefined }); - lastIndex = match.index + match[0].length; - continue; - } - - // Standard text formatting (Group 2 is specs, Group 3 is content) - const specs = match[2].split(',').map(s => s.trim().toLowerCase()); - const content = match[3]; - const segment: TextSegment = { text: content }; - - for (const spec of specs) { - // Text styles - if (spec === 'bold') segment.bold = true; - else if (spec === 'dim') segment.dim = true; - else if (spec === 'italic') segment.italic = true; - else if (spec === 'underline') segment.underline = true; - else if (spec === 'strikethrough' || spec === 'strike') segment.strikethrough = true; - else if (spec === 'overline') segment.overline = true; - // Background color (bg-colorname or bg-#hex) - else if (spec.startsWith('bg-')) { - const bgColor = spec.slice(3); - if (colors[bgColor]) { - segment.background = colors[bgColor]; - } else if (bgColor.startsWith('#')) { - segment.background = bgColor; - } - } - // Foreground color - else if (colors[spec] && !textStyles.includes(spec)) { - segment.color = colors[spec]; - } else if (spec.startsWith('#')) { - segment.color = spec; - } - } - - segments.push(segment); - lastIndex = match.index + match[0].length; - } - - if (lastIndex < text.length) { - segments.push({ text: text.slice(lastIndex) }); - } - - if (segments.length === 0) { - segments.push({ text }); - } - - return segments; -} - -// Get plain text from segments (for length calculation) -export function getPlainText(segments: TextSegment[]): string { - return segments.map(s => s.text).join(''); -} - -// Get segments up to a certain character count (for typing animation) -export function getSegmentsUpToChar(segments: TextSegment[], charCount: number): TextSegment[] { - const result: TextSegment[] = []; - let remaining = charCount; - - for (const segment of segments) { - if (remaining <= 0) break; - - if (segment.text.length <= remaining) { - result.push(segment); - remaining -= segment.text.length; - } else { - // Partial segment - result.push({ ...segment, text: segment.text.slice(0, remaining) }); - remaining = 0; - } - } - - return result; -} - -export function getSegmentStyle(segment: TextSegment): string { - const styles: string[] = []; - if (segment.color) styles.push(`color: ${segment.color}`); - if (segment.background) styles.push(`background-color: ${segment.background}; padding: 0.1em 0.25em; border-radius: 3px`); - if (segment.bold) styles.push('font-weight: bold'); - if (segment.dim) styles.push('opacity: 0.6'); - if (segment.italic) styles.push('font-style: italic'); - - // Combine text decorations - const decorations: string[] = []; - if (segment.underline) decorations.push('underline'); - if (segment.strikethrough) decorations.push('line-through'); - if (segment.overline) decorations.push('overline'); - if (decorations.length > 0) { - styles.push(`text-decoration: ${decorations.join(' ')}`); - } - - return styles.join('; '); -} - -export function getLinePrefix(type: string): string { - switch (type) { - case 'error': - return '✗ '; - case 'success': - return '✓ '; - case 'warning': - return '⚠ '; - case 'info': - return '› '; - default: - return ''; - } -} - -export function getButtonStyle(style?: string): string { - switch (style) { - case 'primary': - return 'var(--terminal-primary)'; - case 'accent': - return 'var(--terminal-accent)'; - case 'warning': - return '#f9e2af'; - case 'error': - return '#f38ba8'; - default: - return 'var(--terminal-text)'; - } -} - -export function parseDimension(value: string | number | undefined): string | undefined { - if (value === undefined) return undefined; - if (typeof value === 'number') { - // If it's a decimal between 0 and 1, treat as percentage - if (value > 0 && value <= 1) { - return `${value * 100}%`; - } - return `${value}px`; - } - if (typeof value === 'string') { - // Handle fractions like "1/2", "1/3" - if (value.includes('/')) { - const [num, den] = value.split('/').map(Number); - if (!isNaN(num) && !isNaN(den) && den !== 0) { - return `${(num / den) * 100}%`; - } - } - // Return as is if it already has a unit or is just a number string - return value; - } - return undefined; -} diff --git a/src/lib/components/ui/LinkButton.svelte b/src/lib/components/ui/LinkButton.svelte new file mode 100644 index 0000000..78b6ec0 --- /dev/null +++ b/src/lib/components/ui/LinkButton.svelte @@ -0,0 +1,52 @@ + + + + {#if icon} + + {/if} + {@render children()} + + + diff --git a/src/lib/components/ui/PageHeader.svelte b/src/lib/components/ui/PageHeader.svelte new file mode 100644 index 0000000..642a7a3 --- /dev/null +++ b/src/lib/components/ui/PageHeader.svelte @@ -0,0 +1,68 @@ + + + + {#if title} + {title} | {profile.displayName} + {/if} + {#if subtitle} + + {/if} + + + + + diff --git a/src/lib/components/ui/Panel.svelte b/src/lib/components/ui/Panel.svelte new file mode 100644 index 0000000..4c68a27 --- /dev/null +++ b/src/lib/components/ui/Panel.svelte @@ -0,0 +1,49 @@ + + + + {#if title} +
+ {#if icon} + + {/if} + {title} +
+ {/if} + {@render children?.()} +
+ + diff --git a/src/lib/components/ui/ProfileIntro.svelte b/src/lib/components/ui/ProfileIntro.svelte new file mode 100644 index 0000000..a1044ca --- /dev/null +++ b/src/lib/components/ui/ProfileIntro.svelte @@ -0,0 +1,48 @@ + + +
+ {profile.displayName} +
+

{heading}

+
{subheading}
+

{text}

+
+
+ + diff --git a/src/lib/components/ui/Section.svelte b/src/lib/components/ui/Section.svelte new file mode 100644 index 0000000..920028b --- /dev/null +++ b/src/lib/components/ui/Section.svelte @@ -0,0 +1,43 @@ + + +
+

{title}

+ {@render children()} +
+ + diff --git a/src/lib/components/ui/SectionNav.svelte b/src/lib/components/ui/SectionNav.svelte new file mode 100644 index 0000000..5145d21 --- /dev/null +++ b/src/lib/components/ui/SectionNav.svelte @@ -0,0 +1,41 @@ + + + + + diff --git a/src/lib/components/ui/Tag.svelte b/src/lib/components/ui/Tag.svelte new file mode 100644 index 0000000..f1c0748 --- /dev/null +++ b/src/lib/components/ui/Tag.svelte @@ -0,0 +1,19 @@ + + +{label} + + diff --git a/src/lib/config.ts b/src/lib/config.ts deleted file mode 100644 index 27a6e9b..0000000 --- a/src/lib/config.ts +++ /dev/null @@ -1,20 +0,0 @@ -// ============================================================================ -// CONFIGURATION - BARREL EXPORT -// ============================================================================ -// This file re-exports all configuration from the modular config/ directory. -// For backward compatibility, you can continue importing from '$lib/config'. -// -// The configuration is now split into smaller, focused files: -// - config/user.ts → User profile and skills -// - config/layout.ts → Layout, breakpoints, fonts, navbar, scrollbar -// - config/theme.ts → Colors, animations, effects, loading screen -// - config/content.ts → Projects, models, hackathon cards -// - config/terminal.ts → Terminal settings, TUI styling, shortcuts -// - config/navigation.ts → Navigation links, site metadata, page meta -// -// You can import directly from individual files for smaller bundles: -// import { user } from '$lib/config/user'; -// import { colorPalette } from '$lib/config/theme'; -// ============================================================================ - -export * from './config/index'; diff --git a/src/lib/config/blog.ts b/src/lib/config/blog.ts deleted file mode 100644 index e397228..0000000 --- a/src/lib/config/blog.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const blogName = 'Finding Out'; -export const blogTagline = 'My Engineering Blog'; diff --git a/src/lib/config/content.ts b/src/lib/config/content.ts deleted file mode 100644 index 3dc945c..0000000 --- a/src/lib/config/content.ts +++ /dev/null @@ -1,360 +0,0 @@ -// ============================================================================ -// PROJECTS -// ============================================================================ - -export interface OpenSourceProject { - name: string; - description: string; - tech: string[]; - github?: string; - live?: string; - image?: string; -} - - -export const openSourceProjects: OpenSourceProject[] = [ - { - name: 'TypstDrive', - description: 'TypstDrive is a self-hosted collaborative web editor for Typst.', - tech: ['Typst', 'Self-Hosted', 'Collaborative'], - github: 'https://github.com/SirBlobby/TypstDrive', - image: '/projects/typstdrive.png', - }, - { - name: 'Typst Desktop', - description: 'Typst Desktop is a native desktop app for writing and compiling Typst documents offline.', - tech: ['Typst', 'Desktop', 'Offline'], - github: 'https://github.com/SirBlobby/typst-desktop', - image: '/projects/typstdesktop.png', - }, - { - name: 'Pkit', - description: 'CLI toolkit and utilities (Rust project) — small developer-focused CLI.', - tech: ['Rust', 'CLI'], - live: 'https://pkit.sirblob.co/', - github: 'https://github.com/dead-projects-inc/pkit-cli', - image: 'https://icons.veryicon.com/png/o/business/vscode-program-item-icon/rust-1.png', - }, - { - name: 'OBookLLM', - description: "OBookLLM is a powerful, self-hosted, offline-capable alternative to NotebookLLM.", - tech: ['Python', 'LLM', 'AI', 'NotebookLLM'], - github: 'https://github.com/SirBlobby/OBookLLM', - image: 'https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg', - } -]; - -// ============================================================================ -// PACKAGES -// ============================================================================ - - -export interface PackageProject { - name: string; - description: string; - tech: string[]; - github?: string; - live?: string; - image?: string; - featured?: boolean; -} - -export const packageProjects: PackageProject[] = [ - { - name: 'PokemonTCGAPI', - description: 'UnOfficial NPM package wrapper for the PokemonTCG API.', - tech: ['TypeScript', 'Node.js'], - live: 'https://www.npmjs.com/package/@bosstop/pokemontcgapi', - image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/1200px-Npm-logo.svg.png', - featured: false - }, - { - name: 'MCSS TS API', - description: 'TypeScript API for MC Server Soft services, published to NPM.', - tech: ['TypeScript', 'Node.js'], - live: 'https://www.npmjs.com/package/@mcserversoft/mcss-api', - image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/1200px-Npm-logo.svg.png', - featured: false - }, - { - name: 'MCP Selenium', - description: 'Selenium automation utilities for MCP workflows, packaged for reuse.', - tech: ['TypeScript', 'Selenium'], - live: 'https://www.npmjs.com/package/@sirblob/mcp-selenium', - image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/1200px-Npm-logo.svg.png', - featured: true - } -]; - -// ============================================================================ -// 3D MODELS -// ============================================================================ - -export interface Model3D { - name: string; - description: string; - thumbnail: string; - modelUrl?: string; // URL to .glb/.gltf file for preview - downloadUrl?: string; - software: string[]; - polyCount?: string; - featured?: boolean; -} - -export const models: Model3D[] = [ - { - name: 'Bake Potato', - description: 'A fun baked-potato 3D model for demos and testing.', - thumbnail: '/models/BakePotato-thumb.png', - modelUrl: '/models/BakePotato.glb', - software: ['Blender'], - polyCount: '1k tris', - featured: true - }, - { - name: 'Soda Can', - description: 'A realistic soda can GLB used for materials/tests.', - thumbnail: '/models/Soda-thumb.png', - modelUrl: '/models/Soda.glb', - software: ['Blender'], - polyCount: '800 tris', - featured: false - } -]; - -// ============================================================================ -// EXPERIENCE -// ============================================================================ - -export interface Experience { - role: string; - organization: string; - employmentType?: string; - location?: string; - period: string; - description?: string; - tech?: string[]; - awards?: string[]; -} - - -export const experience: Experience[] = [ - { - role: 'Research Assistant', - organization: 'College of Engineering and Computing', - employmentType: 'Part-time', - period: 'May 2026 – Present · 3 mos' - }, - { - role: 'Software Engineering Lead', - organization: 'Raytheon Autonomous Vehicle Competition', - period: 'Dec 2025 – May 2026 · 6 mos', - description: - 'Developing a collaborative autonomous system utilizing Python and ROS2 to coordinate communication and control as a UAV scouts and lands on a moving UGV. Engineering Gazebo simulation environments to validate computer vision algorithms for ArUco marker target identification and obstacle avoidance.', - tech: ['Python', 'ROS2'], - awards: ['1st Place Overall Winner'] - }, - { - role: 'QA Automation Intern', - organization: 'Deltek', - employmentType: 'Internship', - location: 'Hybrid', - period: 'Jun 2025 – Aug 2025 · 3 mos', - description: - 'Designed and implemented a workflow integrating AI into the QA automation pipeline, enhancing test efficiency for the GovWin IQ team. Researched Model Context Protocol (MCP) servers to improve automation, developed and published a public Selenium MCP package to NPM, enabling browser automation interactions with Agentic AI.', - tech: ['Artificial Intelligence (AI)', 'TypeScript', 'Selenium'] - } -]; - -// ============================================================================ -// RESEARCH -// ============================================================================ - -export interface ResearchItem { - title: string; - organization?: string; - period?: string; - description?: string; - tech?: string[]; - link?: string; -} - -export const researchItems: ResearchItem[] = [ - { - title: 'Research Assistant', - organization: 'College of Engineering and Computing', - period: 'May 2026 – Present · 3 mos' - } -]; - -// ============================================================================ -// HACKATHONS -// ============================================================================ - -export type Card = { - title: string; - description: string; - image?: string | null; - link?: string; - repo?: string; - devpost?: string; - hackathonName?: string; - university?: string; - location?: string; - year?: string; - tags?: string[]; - featured?: boolean; - awards?: { track: string; place: string }[]; - liveWarning?: boolean; -}; - -export const cards: Card[] = [ - { - image: "/hacks/hushmap.png", - title: "HushMap", - description: - "Architected a real-time campus noise and occupancy monitoring system utilizing SvelteKit, FastAPI, and MongoDB to help students locate quiet study spaces.", - link: "https://hushmap.study/", - devpost: "https://devpost.com/software/hushmap-193nia", - hackathonName: "Bitcamp 2026", - university: "University of Maryland", - location: "College Park, MD", - year: "2026", - tags: ["Svelte", "YOLOv8", "FastAPI", "MongoDB"], - featured: true, - awards: [ - { track: "Best UI/UX Hack", place: "Winner" } - ] - }, - { - image: "/hacks/learningbuddy.png", - title: "LearningBuddy", - description: - "Engineered an ESP32-S3 IoT study companion in C++ with secure Wi-Fi provisioning, utilizing WebSockets and Socket.IO to stream low-latency raw audio for live lecture capture.", - devpost: "https://devpost.com/software/studybuddy-bc0i3j", - hackathonName: "HaxFax x PatriotHacks 2026", - university: "George Mason University", - location: "Fairfax, VA", - year: "2026", - tags: ["C++", "WebSocket", "ESP32-S3", "Socket.IO"], - featured: true, - awards: [ - { track: "Best use of MongoDB", place: "Winner" } - ] - }, - { - image: "/hacks/fooddecisive.png", - title: "Food Decisive", - description: - "Ever felt the indecisiveness of choosing what to eat? Don't fret! Decide your next bite with Food Decisive", - link: "https://fooddecisive.sirblob.co/", - repo: "https://github.com/SirBlobby/VTHacks-12", - devpost: "https://devpost.com/software/food-decisive", - hackathonName: "VTHacks 12", - university: "Virginia Tech", - location: "Blacksburg, VA", - year: "2024", - tags: ["AI", "Llama3.1", "Svelte", "Node.js"], - featured: false, - liveWarning: true - }, - { - image: "/hacks/carbin.png", - title: "Carbin", - description: - "Encourage student participation in responsible waste management with smart bins that guide proper disposal.", - repo: "https://github.com/SirBlobby/patriotHacks2024", - devpost: "https://devpost.com/software/carboniferous-akc4mj", - hackathonName: "PatriotHacks 2024", - university: "George Mason University", - location: "Fairfax, VA", - year: "2024", - tags: ["AI", "Azure", "CloudConvert", "Python", "React", "TypeScript", "API"], - featured: true, - awards: [ - { track: "Save the World", place: "2nd Place" }, - { track: "Microsoft X Cloudforce", place: "2nd Place" } - ] - }, - { - image: "/hacks/patsafe.png", - title: "PatSafe", - description: - "Bridging the gap between doctors and patients for seamless post-discharge care", - link: "https://hoya-hax2025.vercel.app/", - repo: "https://github.com/SirBlobby/HoyaHax2025", - devpost: "https://devpost.com/software/patsafe", - hackathonName: "HoyaHax 2025", - university: "Georgetown University", - location: "Washington, D.C.", - year: "2025", - tags: ["Javascript", "Next.js", "React", "TypeScript", "LangChain", "OpenAI"], - }, - { - image: "https://placehold.co/800x400/334155/94a3b8?text=Fauxcall", - title: "Fauxcall", - description: - "This product is perfect for situations where you are walking at night and you feel unsafe from someone. Fauxcall lets users create a convincing fake phone call to deter potential attackers and provide a quick escape mechanism.", - link: "", - repo: "https://github.com/SirBlobby/HooHacks-12", - devpost: "https://devpost.com/software/fauxcall", - hackathonName: "HooHacks 2025", - university: "University of Virginia", - location: "Charlottesville, VA", - year: "2025", - tags: ["mongodb", "next.js", "python", "react", "sesame.com", "skeleton", "twilio"], - featured: false - }, - { - image: "/hacks/drinkhappy.png", - title: "Drink Happy", - description: - "drinkhappy.tech is a gamified hydration wellness app that helps users track drinks, earn points for healthy choices, and compete with friends. It's powered by Gemini AI for smart drink recognition.", - link: "https://drinkhappy.tech", - repo: "https://github.com/SirBlobby/Bitcamp-2025", - devpost: "https://devpost.com/software/drink-happy", - hackathonName: "Bitcamp", - university: "University of Maryland", - location: "College Park, MD", - year: "2025", - tags: ["api", "auth0", "gemini", "github", "javascript", "mongodb", "nextjs", "react", "tailwind", "vercel"], - featured: false - }, - { - image: "/hacks/roadcast.png", - title: "Roadcast", - description: - "Roadcast provides drivers with crash data and weather insights to choose safer routes home. The project combines historical crash data, weather feeds, and spatial analysis to surface hazardous areas and route-level safety recommendations.", - link: "https://roadcast.sirblob.co/", - repo: "https://github.com/SirBlobby/roadcast", - devpost: "https://devpost.com/software/roadcast", - hackathonName: "VTHacks 13", - university: "Virginia Tech", - location: "Blacksburg, VA", - year: "2025", - tags: ["react", "node.js", "python", "mongodb", "geospatial", "mapbox"], - featured: false, - liveWarning: true - }, - { - image: "/img/hoya26.png", - title: "Ethix", - description: "A sustainability platform that helps users detect and report greenwashing - misleading environmental claims made by companies and products.", - repo: "https://github.com/SirBlobby/Hoya26", - devpost: "https://devpost.com/software/ethix", - link: "https://ethix.sirblob.co/", - hackathonName: "HoyaHax 2026", - university: "Georgetown University", - location: "Washington, D.C.", - year: "2026", - tags: ["Svelte", "TypeScript", "Python", "Flask", "Tailwind", "Sustainability"], - featured: true - } -]; - -// Sort cards to show featured first -export const sortedCards = [...cards].sort((a, b) => { - if (a.featured && !b.featured) return -1; - if (!a.featured && b.featured) return 1; - return 0; -}); diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts deleted file mode 100644 index 3b074af..0000000 --- a/src/lib/config/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -// ============================================================================ -// CONFIGURATION INDEX -// ============================================================================ -// This file re-exports all configuration modules for backward compatibility. -// You can import from here or directly from the individual config files. -// -// Import examples: -// import { user, skills } from '$lib/config'; // via barrel -// import { user } from '$lib/config/user'; // direct import -// import { colorPalette } from '$lib/config/theme'; // direct import -// ============================================================================ - -// User profile and skills -export type { LinkItem } from './user'; -export { user, skills, links } from './user'; - -// Layout, breakpoints, fonts, navbar, scrollbar -export { layout, breakpoints, fonts, navbar, scrollbar } from './layout'; - -// Theme colors, buttons, animations, effects, loading screen -export { - colorPalette, - terminalButtons, - loadingScreen, - effects, - animations -} from './theme'; - -// Content: projects, models, experience, research, hackathon cards -export type { OpenSourceProject, PackageProject, Model3D, Experience, ResearchItem, Card } from './content'; -export { openSourceProjects, packageProjects, models, experience, researchItems, cards, sortedCards } from './content'; - -// Terminal settings, TUI styling, speed presets, model viewer, particles, shortcuts -export type { SpeedPreset, TerminalSettings } from './terminal'; -export { - terminalSettings, - tuiStyle, - tuiText, - pageSpeedSettings, - pageAutoscrollSettings, - speedPresets, - modelViewer, - particles, - keyboardShortcuts -} from './terminal'; - -// Navigation, site metadata, page meta -export type { PageMeta } from './navigation'; -export { navigation, site, pageMeta } from './navigation'; - -// ============================================================================ -// COLOR FORMATTING REFERENCE -// ============================================================================ -// Use (&color)text(&) syntax in terminal content for colored text -// Use (&icon, iconName) syntax for inline icons -// Use (&icon, iconName, size) to set a custom icon size in pixels -// -// Available colors: -// red, green, yellow, blue, magenta, cyan, white, gray, orange, pink -// primary, accent, muted, error, success, warning, info -// -// Available styles: -// bold, dim, italic, underline -// -// Examples: -// "Hello (&red)world(&)!" -> red "world" -// "(&bold,green)Success(&): Task done" -> bold green "Success" -// "(&italic,cyan)Note(&): See docs" -> italic cyan "Note" -// "Status: (&primary)Active(&)" -> theme primary color -// "(&dim)This is muted text(&)" -> dimmed text -// "(&bold,underline,yellow)Warning(&)" -> bold underlined yellow -// "(&error)Failed(&) - (&success)Passed(&)" -> multiple colors -// "(&icon, mdi:github) GitHub" -> inline GitHub icon -// "Check (&icon, mdi:check) Done" -> inline check icon -// "(&icon, mdi:trophy, 32) Winner" -> bigger trophy icon (32px) - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ -// NOTE: Helper functions are in `src/lib/index.ts` to provide a central -// barrel and avoid duplication. Import helpers from `$lib` instead of `$lib/config`. diff --git a/src/lib/config/layout.ts b/src/lib/config/layout.ts deleted file mode 100644 index 3614239..0000000 --- a/src/lib/config/layout.ts +++ /dev/null @@ -1,94 +0,0 @@ -// ============================================================================ -// LAYOUT & DIMENSIONS -// ============================================================================ - -export const layout = { - // Navbar - navbarHeight: 60, // px - navbarMaxWidth: 1400, // px - navbarZIndex: 1000, - - // Container - containerMaxWidth: 1200, // px - containerPadding: '1.5rem', - - // Grid - gridGap: '1.5rem', - gridMinColumnWidth: 300, // px - - // Page margins - pageBottomMargin: 80, // px (desktop) - pageMobileBottomMargin: 60, // px (mobile) -}; - -// ============================================================================ -// RESPONSIVE BREAKPOINTS -// ============================================================================ - -export const breakpoints = { - mobile: 768, // px - tablet: 1024, // px - desktop: 1400, // px -}; - -// ============================================================================ -// FONTS -// ============================================================================ - -export const fonts = { - mono: "'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', monospace", - sans: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", - monoWeight: 600, -}; - -// ============================================================================ -// NAVBAR -// ============================================================================ - -export const navbar = { - height: 60, // px - also update layout.navbarHeight if changing - maxWidth: 1400, // px - padding: '0.75rem 1.5rem', - gap: '2rem', - zIndex: 1000, - - // Brand - brandFontSize: '0.9rem', - cursorWidth: 8, // px - - // Hamburger menu - hamburgerWidth: 24, // px - hamburgerHeight: 2, // px - hamburgerSpacing: 7, // px - - // Links - linksGap: '1.5rem', - linkFontSize: '0.85rem', - linkPadding: '0.5rem 0.75rem', - linkBorderRadius: 4, // px - linkUnderlineHeight: 2, // px - - // Mode toggle - modeToggleSize: 40, // px - modeToggleBorderRadius: 8, // px - themeButtonBorderRadius: 6, // px - - // Dropdown - dropdownMinWidth: 180, // px - dropdownBorderRadius: 8, // px - dropdownZIndex: 1001, - backdropZIndex: 999, - mobileExpandedHeight: 200, // px -}; - -// ============================================================================ -// SCROLLBAR -// ============================================================================ - -export const scrollbar = { - width: 8, // px - borderRadius: 4, // px - trackColor: 'rgba(0, 0, 0, 0.1)', - thumbColor: 'rgba(128, 128, 128, 0.5)', - thumbHoverColor: 'rgba(128, 128, 128, 0.7)', -}; diff --git a/src/lib/config/navigation.ts b/src/lib/config/navigation.ts deleted file mode 100644 index f6f66fc..0000000 --- a/src/lib/config/navigation.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { user, skills } from './user'; - -// ============================================================================ -// NAVIGATION -// ============================================================================ - -export const navigation = [ - { name: 'home', path: '/', icon: '~' }, - { name: 'about', path: '/about', icon: 'mdi:account' }, - // { name: 'portfolio', path: '/portfolio', icon: '📁' }, - // { name: 'models', path: '/models', icon: '🎨' }, - { name: 'projects', path: '/projects', icon: '🏆' }, - // { name: 'components', path: '/components', icon: '🧩' }, - { name: 'blog', path: '/blog', icon: '📝' } -]; - -// ============================================================================ -// SITE METADATA -// ============================================================================ - -export const site = { - title: `${user.displayname} | Portfolio`, - description: `${user.title} - ${user.bio}`, - keywords: ['developer', 'portfolio', 'programming', ...skills.languages, ...skills.frameworks], - ogImage: '/og-image.png', - twitterHandle: '@yourusername' -}; - -// ============================================================================ -// PAGE META (per-route) -// ============================================================================ - -export interface PageMeta { - title: string; - description?: string; - icon?: string; // Iconify id or path to an image (e.g. 'mdi:home' or '/icons/home.svg') - keywords?: string[]; - ogImage?: string; -} - -// Map route => meta. Use route paths as keys (e.g. '/', '/portfolio') -export const pageMeta: Record = { - '/': { - title: `${user.displayname} — Home`, - description: `Home — ${user.title} portfolio and projects.`, - icon: 'mdi:home', - keywords: ['home', 'portfolio', 'about'] - }, - '/about': { - title: `${user.displayname} — About`, - description: `About — ${user.title} portfolio and projects.`, - icon: 'mdi:account', - keywords: ['about', 'portfolio', 'about'] - }, - '/portfolio': { - title: `${user.displayname} — Portfolio`, - description: 'Selected projects, highlights and case studies.', - icon: 'mdi:folder-multiple', - keywords: ['projects', 'portfolio', 'showcase'] - }, - '/models': { - title: `${user.displayname} — 3D Models`, - description: 'A curated collection of 3D models and previews.', - icon: 'mdi:cube-outline', - keywords: ['3d', 'models', 'glb', 'gltf'] - }, - '/projects': { - title: `${user.displayname} — Projects`, - description: 'Hackathon projects, demos and awards.', - icon: 'mdi:trophy', - keywords: ['hackathon', 'projects', 'events'] - }, - '/projects/opensource': { - title: `${user.displayname} — Open Source`, - description: 'Libraries and tools built and maintained in the open.', - icon: 'mdi:source-branch', - keywords: ['open source', 'projects', 'github'] - }, - '/projects/packages': { - title: `${user.displayname} — Packages`, - description: 'Published packages and CLI tools.', - icon: 'mdi:package-variant', - keywords: ['packages', 'npm', 'cli'] - }, - '/projects/hackathons': { - title: `${user.displayname} — Hackathons`, - description: 'Hackathon projects, demos and awards.', - icon: 'mdi:trophy', - keywords: ['hackathon', 'projects', 'events'] - }, - '/projects/research': { - title: `${user.displayname} — Research`, - description: 'Research roles, projects, and publications.', - icon: 'mdi:microscope', - keywords: ['research', 'academic', 'publications'] - }, - '/components': { - title: `${user.displayname} — Components`, - description: 'Terminal UI components showcase and documentation.', - icon: 'mdi:puzzle', - keywords: ['components', 'ui', 'terminal', 'tui'] - }, - '/links': { - title: `${user.displayname} — Links`, - description: 'All my links with scannable QR codes.', - icon: 'mdi:link-variant', - keywords: ['links', 'qr', 'social', 'contact'] - }, - '/blog': { - title: `${user.displayname} — Blog`, - description: 'Notes and write-ups from my projects.', - icon: 'mdi:post-outline', - keywords: ['blog', 'writing', 'notes'] - } -}; diff --git a/src/lib/config/terminal.ts b/src/lib/config/terminal.ts deleted file mode 100644 index 69c43c3..0000000 --- a/src/lib/config/terminal.ts +++ /dev/null @@ -1,261 +0,0 @@ -// ============================================================================ -// TERMINAL DISPLAY SETTINGS -// ============================================================================ - -export type SpeedPreset = 'instant' | 'fast' | 'normal' | 'slow' | 'typewriter'; - -export interface TerminalSettings { - baseTypeSpeed: number; - minTypeSpeed: number; - maxTypeSpeed: number; - startDelay: number; - lineDelay: number; - showCursor: boolean; - promptStyle: 'full' | 'short' | 'minimal'; - icon: string; - scrollMargin: number; -} - -export const terminalSettings: TerminalSettings = { - // Base typing speed in ms per character (will be adjusted by content length) - baseTypeSpeed: 20, - // Minimum typing speed (fastest) - minTypeSpeed: 5, - // Maximum typing speed (slowest) - maxTypeSpeed: 50, - // Delay before starting to type (ms) - startDelay: 300, - // Delay between lines (ms) - lineDelay: 100, - // Show cursor - showCursor: true, - // Prompt style - promptStyle: 'full', - // Terminal icon (emoji or text) - icon: '🐧', - // Scroll margin when navigating buttons (px) - scrollMargin: 80, -}; - -// ============================================================================ -// TUI (Terminal UI) STYLING -// ============================================================================ - -export const tuiStyle = { - // Border & container - borderRadius: 8, // px - borderWidth: 2, // px - width: '95%', - borderGlowOpacity: 0.5, - borderGlowBlur: 4, // px - - // Header - headerPadding: '0.5rem 0.75rem', - headerFontSize: '0.8rem', - - // Body - bodyPadding: '1rem 1.25rem 2rem 1.25rem', - bodyFontSize: '0.9rem', - lineHeight: 1.7, - lineMinHeight: '1.7em', - blankLineHeight: '0.5em', - - // Divider - dividerMargin: '0.75rem 0', - - // Images - imageBorderRadius: 6, // px - defaultImageWidth: 300, // px - - // Cursor - cursorWidth: 8, // px - - // Scrollbar - scrollbarWidth: 6, // px - - // Footer/Statusbar - statusbarPadding: '0.4rem 0.75rem', - statusbarFontSize: '0.75rem', - hintFontSize: '0.7rem', - hintBorderRadius: 3, // px - - // Buttons - buttonPadding: '0.5rem 0.75rem', - buttonMargin: '0.2rem 0', - buttonBorderRadius: 4, // px - buttonFontSize: '0.9rem', - buttonIndicatorFontSize: '0.8rem', - buttonIndicatorWidth: '1rem', -}; - -// ============================================================================ -// TUI TEXT & INDICATORS -// ============================================================================ - -export const tuiText = { - // Status text - loading: 'Loading...', - ready: 'Ready', - skipButton: 'Skip (Y)', - tuiLabel: 'TUI', - - // Keyboard hints - hints: { - navigate: '↑↓ navigate', - select: '⏎ select', - toggle: 'T theme', - }, - - // Line prefixes - prefixes: { - error: '✗ ', - success: '✓ ', - info: '› ', - warning: '⚠ ', - command: '$ ', - }, - - // Button indicators - buttonIndicator: { - selected: '▶', - unselected: ' ', - }, -}; - -// ============================================================================ -// Per-page speed settings (override global settings) -// Use preset names or custom multiplier (0.1 = 10x faster, 2 = 2x slower) -// ============================================================================ - -export const pageSpeedSettings: Record = { - // Examples: - // 'home': 'fast', // Fast typing on home page - // 'portfolio': 'normal', // Normal speed - // 'models': 'instant', // No typing animation - // 'hackathons': 0.5, // Custom: 2x faster than normal - 'home': 'fast', - 'about': 'normal', - 'portfolio': 'instant', - 'models': 'fast', - 'projects': 'fast', - 'opensource': 'fast', - 'packages': 'fast', - 'hackathons': 'fast', - 'research': 'fast', - 'components': 'fast' -}; - -// Per-page autoscroll settings (whether to auto-scroll as content types) -// Set to false to disable autoscroll on specific pages -export const pageAutoscrollSettings: Record = { - // Examples: - // 'home': true, // Enable autoscroll - // 'portfolio': false, // Disable autoscroll - 'home': true, - 'about': false, - 'portfolio': false, - 'models': false, - 'projects': false, - 'opensource': false, - 'packages': false, - 'hackathons': false, - 'research': false, - 'components': false -}; - -// Speed preset multipliers (lower = faster) -export const speedPresets: Record = { - 'instant': 0, // No delay, instant display - 'fast': 0.1, // 3x faster - 'normal': 0.5, // Default speed - 'slow': 1.5, // 2x slower - 'typewriter': 2 // 3x slower, classic typewriter feel -}; - -// ============================================================================ -// MODEL VIEWER (3D) -// ============================================================================ - -export const modelViewer = { - // Container - borderRadius: 8, // px - minHeight: 300, // px - headerPadding: '0.5rem 0.75rem', - nameFontSize: '0.85rem', - - // Controls - controlButtonSize: 28, // px - controlButtonBorderRadius: 4, // px - fullscreenZIndex: 100, - fullscreenTopOffset: 60, // px (navbar height) - - // Camera - cameraFov: 45, - cameraNear: 0.1, - cameraFar: 1000, - cameraZ: 2.5, // initial camera distance - - // Controls behavior - controlsMinDistance: 1, - controlsMaxDistance: 10, - dampingFactor: 0.05, - autoRotateSpeed: 2, - - // Model - modelScale: 1.5, - - // Ground plane - groundColor: 0x222222, - groundOpacity: 0.3, - - // Lighting - ambientIntensity: 0.6, - directionalIntensity: 0.8, - lightColor: 0xffffff, - rimLightColor: 0x88ccff, - - // Text - loadingText: 'Loading model...', - errorText: 'Failed to load model', - hints: { - rotate: 'Drag to rotate', - zoom: 'Scroll to zoom', - }, - - // Timing - resizeTimeout: 100, // ms -}; - -// ============================================================================ -// PARTICLES (3D Background) -// ============================================================================ - -export const particles = { - count: 400, - spreadArea: 20, - size: 0.1, - velocityRange: 0.01, - - // Opacity - darkOpacity: 0.6, - lightOpacity: 0.4, - - // Motion - waveAmplitude: 0.001, - waveFrequency: 0.1, - boundary: 10, // wrap boundary - rotationSpeedY: 0.05, - rotationSpeedX: 0.02, -}; - -// ============================================================================ -// KEYBOARD SHORTCUTS -// ============================================================================ - -export const keyboardShortcuts = { - skip: ['y', 'Y'], // Skip typing animation - toggleTheme: ['t', 'T'], // Toggle dark/light mode - navigateUp: ['ArrowUp', 'k'], - navigateDown: ['ArrowDown', 'j'], - select: ['Enter'], -}; diff --git a/src/lib/config/theme.ts b/src/lib/config/theme.ts deleted file mode 100644 index 150b40f..0000000 --- a/src/lib/config/theme.ts +++ /dev/null @@ -1,89 +0,0 @@ -// ============================================================================ -// COLOR PALETTE (DEPRECATED) -// ============================================================================ -// NOTE: This is deprecated. Use themeColors.colorMap from $lib/stores/theme instead. -// The colorMap in theme JSON files is the source of truth and supports dynamic themes. -// This is kept for backward compatibility only. - -export const colorPalette = { - // Basic colors - red: '#f38ba8', - green: '#a6e3a1', - yellow: '#f9e2af', - blue: '#89b4fa', - magenta: '#cba6f7', - cyan: '#94e2d5', - white: '#cdd6f4', - gray: '#6c7086', - orange: '#fab387', - pink: '#f5c2e7', - black: '#1e1e2e', - surface: '#313244', - - // Semantic colors (map to basic colors by default) - error: '#f38ba8', // red - success: '#a6e3a1', // green - warning: '#f9e2af', // yellow - info: '#89b4fa', // blue -}; - -// ============================================================================ -// TERMINAL WINDOW BUTTONS (traffic lights) -// ============================================================================ - -export const terminalButtons = { - close: '#ff5f56', - minimize: '#ffbd2e', - maximize: '#27ca40', -}; - -// ============================================================================ -// LOADING SCREEN -// ============================================================================ - -export const loadingScreen = { - background: '#0d1117', - textColor: '#c9d1d9', - cursorColor: '#1793d1', - text: "Blob's Portfolio", -}; - -// ============================================================================ -// EFFECTS -// ============================================================================ - -export const effects = { - backdropBlur: 10, // px - selectionBackground: 'rgba(23, 147, 209, 0.3)', -}; - -// ============================================================================ -// ANIMATIONS -// ============================================================================ - -export const animations = { - // Terminal/TUI animations - terminalFadeIn: { duration: '0.4s', easing: 'ease-out' }, - tuiFadeIn: { duration: '0.4s', easing: 'ease-out' }, - lineSlideIn: { duration: '0.15s', easing: 'ease-out' }, - cursorBlink: { duration: '1s' }, - borderGlow: { duration: '8s', transition: '0.3s' }, - - // Button/interaction animations - buttonHover: { duration: '0.2s' }, - buttonTransition: { duration: '0.15s' }, - - // Navigation animations - hamburgerTransition: { duration: '0.3s' }, - navLinkTransition: { duration: '0.2s' }, - linkUnderline: { duration: '0.3s' }, - dropdown: { flyDuration: 200, fadeDuration: 150 }, // ms - - // Model viewer - modelViewerTransition: { duration: '0.3s' }, - spin: { duration: '1s' }, - - // General - pulse: { duration: '2s' }, - float: { duration: '3s' }, -}; diff --git a/src/lib/config/user.ts b/src/lib/config/user.ts deleted file mode 100644 index 023660e..0000000 --- a/src/lib/config/user.ts +++ /dev/null @@ -1,60 +0,0 @@ -// ============================================================================ -// USER PROFILE -// ============================================================================ - -export const user = { - name: 'Gagan "Adith" M', - displayname: 'Sir Blob', - username: 'sirblob', - hostname: 'engineering', - title: 'Engineering Student', - email: 'sirblob0@gmail.com', - location: 'USA (EAST)', - bio: `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. ` + - `I'm interested in Open Source, Game Development, Embedded Systems, and AI/ML.`, - - // Prefer an absolute avatar URL if you want to pull directly from GitHub - avatar: '/blob_nerd.png', - - // Social links - array of { name, icon (Iconify), link } - socials: [ - { name: 'GitHub', icon: 'mdi:github', link: 'https://github.com/SirBlobby' }, - { name: 'LinkedIn', icon: 'mdi:linkedin', link: 'https://www.linkedin.com/in/gmanjunatha/' }, - { name: 'Devpost', icon: 'simple-icons:devpost', link: 'https://devpost.com/Sir_Blob_' }, - { name: 'Discord', icon: 'ic:baseline-discord', link: 'https://discord.com/users/sir_blob_' } - ] -}; - -// ============================================================================ -// LINKS PAGE -// ============================================================================ - -export interface LinkItem { - name: string; - icon: string; - link: string; -} - -export const links: LinkItem[] = [ - { name: 'GitHub', icon: 'mdi:github', link: 'https://github.com/SirBlobby' }, - { name: 'LinkedIn', icon: 'mdi:linkedin', link: 'https://www.linkedin.com/in/gmanjunatha/' }, - { name: 'Devpost', icon: 'simple-icons:devpost', link: 'https://devpost.com/Sir_Blob_' }, - { name: 'Discord', icon: 'ic:baseline-discord', link: 'https://discord.com/users/sir_blob_' }, - { name: 'Email', icon: 'mdi:email', link: `mailto:${user.email}` }, - // { name: 'Blog', icon: 'mdi:rss', link: 'https://blog.sirblob.co' } -]; - -// ============================================================================ -// SKILLS -// ============================================================================ - -export const skills = { - languages: ['Python', 'JavaScript', 'TypeScript', 'C', 'C++', 'Java', 'Node.js', 'Rust', 'Go'], - frameworks: ['React', 'Electron', 'Svelte', 'Bootstrap', 'TailwindCSS', 'Discord.js', ], - applications: ['IntelliJ', 'VS Code', 'Git', 'Blender', 'Godot'], - platforms: ['Windows', 'Linux', 'macOS', 'Arduino', 'Raspberry Pi'], - tools: ['Git', 'Docker', 'Neovim', 'VS Code'], - databases: ['MongoDB', 'SQLite'], - interests: ['Open Source', 'Game Development', 'Embedded Systems', 'AI/ML'] -}; diff --git a/src/lib/content/index.ts b/src/lib/content/index.ts new file mode 100644 index 0000000..9d18042 --- /dev/null +++ b/src/lib/content/index.ts @@ -0,0 +1,76 @@ +import profileData from '#content/profile.json'; +import experienceData from '#content/experience.json'; +import leadershipData from '#content/leadership.json'; +import aboutData from '#content/about.json'; +import modelsData from '#content/models.json'; +import desktopData from '#content/desktop.json'; +import blogData from '#content/blog.json'; +import openSourceData from '#content/projects/opensource.json'; +import packagesData from '#content/projects/packages.json'; +import hackathonsData from '#content/projects/hackathons.json'; +import researchData from '#content/projects/research.json'; +import type { + AboutContent, + BlogSettings, + DesktopContent, + Experience, + Leadership, + Model3D, + Profile, + Project, + ProjectCategory, + ProjectCategoryId +} from './types'; + +export type * from './types'; + +export const profile: Profile = profileData; +export const experience: Experience[] = experienceData; +export const leadership: Leadership[] = leadershipData; +export const about: AboutContent = aboutData; +export const models: Model3D[] = modelsData; +export const desktop: DesktopContent = desktopData; +export const blogSettings: BlogSettings = blogData; + +function featuredFirst(projects: Project[]): Project[] { + return [...projects].sort((first, second) => Number(!!second.featured) - Number(!!first.featured)); +} + +export const projectCategories: ProjectCategory[] = [ + { + id: 'opensource', + title: 'Open Source', + icon: 'ph:git-branch-duotone', + summary: 'Libraries and tools I build and maintain in the open.', + projects: featuredFirst(openSourceData) + }, + { + id: 'packages', + title: 'Packages', + icon: 'ph:package-duotone', + summary: 'Published packages and CLI tools you can install directly.', + projects: featuredFirst(packagesData) + }, + { + id: 'hackathons', + title: 'Hackathons', + icon: 'ph:trophy-duotone', + summary: 'Weekend builds, demos, and the awards they picked up.', + projects: featuredFirst(hackathonsData) + }, + { + id: 'research', + title: 'Research', + icon: 'ph:microscope-duotone', + summary: 'Research roles, projects, and publications.', + projects: featuredFirst(researchData) + } +]; + +export function findProjectCategory(id: string): ProjectCategory | undefined { + return projectCategories.find((category) => category.id === id); +} + +export function isProjectCategoryId(id: string): id is ProjectCategoryId { + return projectCategories.some((category) => category.id === id); +} diff --git a/src/lib/content/types.ts b/src/lib/content/types.ts new file mode 100644 index 0000000..fbddd4e --- /dev/null +++ b/src/lib/content/types.ts @@ -0,0 +1,154 @@ +export interface Social { + name: string; + icon: string; + url: string; +} + +export interface Education { + school: string; + degree: string; + graduation: string; +} + +export interface Profile { + name: string; + displayName: string; + username: string; + hostname: string; + title: string; + email: string; + avatar: string; + bio: string; + tagline: string; + education: Education; + socials: Social[]; + skills: Record; + interests: string[]; +} + +export interface LabeledLink { + label: string; + url: string; +} + +export interface Experience { + role: string; + organization: string; + employmentType?: string; + period: string; + highlights: string[]; + tech?: string[]; + awards?: string[]; + links?: LabeledLink[]; +} + +export interface Leadership { + role: string; + organization: string; + url?: string; +} + +export interface ProjectLinks { + demo?: string; + code?: string; + devpost?: string; + package?: string; + article?: string; +} + +export interface HackathonEvent { + name: string; + university?: string; + location?: string; + year?: string; +} + +export interface Award { + track: string; + place: string; +} + +export interface Project { + name: string; + description: string; + tags: string[]; + links: ProjectLinks; + image?: string; + featured?: boolean; + version?: string; + organization?: string; + period?: string; + event?: HackathonEvent; + awards?: Award[]; + demoMayBeOffline?: boolean; +} + +export type ProjectCategoryId = 'opensource' | 'packages' | 'hackathons' | 'research'; + +export interface ProjectCategory { + id: ProjectCategoryId; + title: string; + icon: string; + summary: string; + projects: Project[]; +} + +export interface Hobby { + name: string; + icon: string; + description: string; +} + +export interface Game { + name: string; + image: string; + url: string; +} + +export interface RatedTitle { + name: string; + rating: number; + url: string; +} + +export interface AboutContent { + hobbies: Hobby[]; + games: Game[]; + shows: RatedTitle[]; + movies: RatedTitle[]; +} + +export interface Model3D { + name: string; + description: string; + file: string; + software: string[]; + polyCount?: string; +} + +export type DesktopAction = { page: string } | { terminal: string } | { url: string }; + +export interface DesktopIcon { + label: string; + icon: string; + open: DesktopAction; +} + +export interface Workspace { + number: number; + name: string; + path: string; +} + +export interface DesktopContent { + defaultTheme: string; + workspaces: Workspace[]; + icons: DesktopIcon[]; + startupCommand: string; +} + +export interface BlogSettings { + name: string; + tagline: string; + postsPerPage: number; +} diff --git a/src/lib/desktop/actions.ts b/src/lib/desktop/actions.ts new file mode 100644 index 0000000..f2dbbca --- /dev/null +++ b/src/lib/desktop/actions.ts @@ -0,0 +1,37 @@ +import { goto } from '$app/navigation'; +import { desktop, type DesktopAction, type Workspace } from '#lib/content'; +import { pageWindowId, windowManager } from './windows.svelte'; + +export function isExternalUrl(url: string): boolean { + return /^(https?:|mailto:)/.test(url); +} + +export function openUrl(url: string) { + if (isExternalUrl(url)) { + window.open(url, '_blank', 'noopener,noreferrer'); + return; + } + openPage(url); +} + +export function openPage(path: string) { + windowManager.focus(pageWindowId); + goto(path); +} + +export function runDesktopAction(action: DesktopAction) { + windowManager.launcherOpen = false; + if ('page' in action) openPage(action.page); + else if ('terminal' in action) windowManager.openTerminal(action.terminal); + else openUrl(action.url); +} + +export function closePage() { + goto('/'); +} + +export function activeWorkspace(pathname: string): Workspace | undefined { + return desktop.workspaces + .filter((workspace) => workspace.path === '/' ? pathname === '/' : pathname.startsWith(workspace.path)) + .sort((first, second) => second.path.length - first.path.length)[0]; +} diff --git a/src/lib/desktop/battery.ts b/src/lib/desktop/battery.ts new file mode 100644 index 0000000..fb30391 --- /dev/null +++ b/src/lib/desktop/battery.ts @@ -0,0 +1,43 @@ +const hour = 3600 * 1000; +const dischargeDuration = 5 * hour; +const chargeDuration = 1 * hour; +const cycleDuration = dischargeDuration + chargeDuration; + +export interface BatteryState { + level: number; + charging: boolean; +} + +function easternWallClockMs(date: Date): number { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: 'America/New_York', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23' + }).formatToParts(date); + const part = (type: string) => Number(parts.find((item) => item.type === type)?.value); + return Date.UTC(part('year'), part('month') - 1, part('day'), part('hour'), part('minute'), part('second')); +} + +export function batteryAt(date: Date): BatteryState { + const position = easternWallClockMs(date) % cycleDuration; + + if (position < dischargeDuration) { + return { level: Math.round(100 - (position / dischargeDuration) * 100), charging: false }; + } + return { level: Math.round(((position - dischargeDuration) / chargeDuration) * 100), charging: true }; +} + +export function batteryIcon({ level, charging }: BatteryState): string { + if (charging) return 'mdi:battery-charging'; + if (level > 90) return 'mdi:battery'; + if (level > 70) return 'mdi:battery-80'; + if (level > 50) return 'mdi:battery-60'; + if (level > 30) return 'mdi:battery-40'; + if (level > 10) return 'mdi:battery-20'; + return 'mdi:battery-alert'; +} diff --git a/src/lib/desktop/keybinds.ts b/src/lib/desktop/keybinds.ts new file mode 100644 index 0000000..5d793a2 --- /dev/null +++ b/src/lib/desktop/keybinds.ts @@ -0,0 +1,28 @@ +import { desktop } from '#lib/content'; +import { themeState } from '#lib/theme/theme.svelte'; +import { closePage, openPage } from './actions'; +import { pageWindowId, windowManager } from './windows.svelte'; + +function closeFocusedWindow() { + if (windowManager.focusedId === pageWindowId) closePage(); + else windowManager.closeTerminal(windowManager.focusedId); +} + +export function handleKeybind(event: KeyboardEvent, openWindowIds: string[]) { + if (!event.altKey || event.ctrlKey || event.metaKey) return; + + const key = event.key.toLowerCase(); + const workspace = desktop.workspaces.find((item) => event.code === `Digit${item.number}`); + + if (workspace) openPage(workspace.path); + else if (key === 'enter') windowManager.openTerminal(); + else if (event.code === 'Space') windowManager.launcherOpen = true; + else if (event.code === 'KeyQ') closeFocusedWindow(); + else if (event.code === 'KeyF' && windowManager.focusedId === pageWindowId) windowManager.toggleMaximize(pageWindowId); + else if (event.code === 'KeyJ') windowManager.cycleFocus(openWindowIds, 1); + else if (event.code === 'KeyK') windowManager.cycleFocus(openWindowIds, -1); + else if (event.code === 'KeyT') themeState.cycle(1); + else return; + + event.preventDefault(); +} diff --git a/src/lib/desktop/tiling.ts b/src/lib/desktop/tiling.ts new file mode 100644 index 0000000..dcb1d57 --- /dev/null +++ b/src/lib/desktop/tiling.ts @@ -0,0 +1,49 @@ +export interface TileRect { + x: number; + y: number; + width: number; + height: number; +} + +const fullRect: TileRect = { x: 0, y: 0, width: 1, height: 1 }; + +export function dwindleLayout(windowCount: number, areaWidth: number, areaHeight: number): TileRect[] { + const tiles: TileRect[] = []; + let remaining = { ...fullRect }; + + for (let index = 0; index < windowCount; index++) { + const isLast = index === windowCount - 1; + if (isLast) { + tiles.push(remaining); + break; + } + + const splitSideBySide = remaining.width * areaWidth >= remaining.height * areaHeight; + if (splitSideBySide) { + const halfWidth = remaining.width / 2; + tiles.push({ ...remaining, width: halfWidth }); + remaining = { ...remaining, x: remaining.x + halfWidth, width: halfWidth }; + } else { + const halfHeight = remaining.height / 2; + tiles.push({ ...remaining, height: halfHeight }); + remaining = { ...remaining, y: remaining.y + halfHeight, height: halfHeight }; + } + } + + return tiles; +} + +export function fullscreenLayout(): TileRect { + return { ...fullRect }; +} + +export function tileStyle(rect: TileRect, gapInPixels: number): string { + const halfGap = gapInPixels / 2; + const percent = (value: number) => `${value * 100}%`; + return [ + `left: calc(${percent(rect.x)} + ${halfGap}px)`, + `top: calc(${percent(rect.y)} + ${halfGap}px)`, + `width: calc(${percent(rect.width)} - ${gapInPixels}px)`, + `height: calc(${percent(rect.height)} - ${gapInPixels}px)` + ].join('; '); +} diff --git a/src/lib/desktop/windows.svelte.ts b/src/lib/desktop/windows.svelte.ts new file mode 100644 index 0000000..3f0ba1d --- /dev/null +++ b/src/lib/desktop/windows.svelte.ts @@ -0,0 +1,52 @@ +export const pageWindowId = 'page'; + +export interface TerminalWindow { + id: string; + startupCommand: string; +} + +class WindowManager { + terminals = $state([]); + focusedId = $state(pageWindowId); + maximizedId = $state(null); + launcherOpen = $state(false); + + private nextTerminalNumber = 1; + + openTerminal(startupCommand = '') { + const terminal = { id: `kitty-${this.nextTerminalNumber++}`, startupCommand }; + this.terminals.push(terminal); + this.focus(terminal.id); + } + + closeTerminal(id: string) { + this.terminals = this.terminals.filter((terminal) => terminal.id !== id); + if (this.maximizedId === id) this.maximizedId = null; + if (this.focusedId === id) this.focusedId = this.terminals.at(-1)?.id ?? pageWindowId; + } + + closeAllTerminals() { + this.terminals = []; + this.maximizedId = null; + this.focusedId = pageWindowId; + } + + focus(id: string) { + this.focusedId = id; + if (this.maximizedId && this.maximizedId !== id) this.maximizedId = null; + } + + toggleMaximize(id: string) { + this.maximizedId = this.maximizedId === id ? null : id; + this.focusedId = id; + } + + cycleFocus(openWindowIds: string[], step: number) { + if (openWindowIds.length === 0) return; + const index = openWindowIds.indexOf(this.focusedId); + const nextIndex = (index + step + openWindowIds.length) % openWindowIds.length; + this.focus(openWindowIds[nextIndex]); + } +} + +export const windowManager = new WindowManager(); diff --git a/src/lib/discord/fetchUserImages.ts b/src/lib/discord/fetchUserImages.ts deleted file mode 100644 index 065de30..0000000 --- a/src/lib/discord/fetchUserImages.ts +++ /dev/null @@ -1,149 +0,0 @@ -export enum ImageSize { - USER_AVATAR = 32, - USER_DECORATION = 64, - SERVER_TAG = 32, - ACTIVITY_LARGE = 128, - ACTIVITY_SMALL = 64, - EMOJI = 32, -} - -export type Activity = { - name?: string; - type?: number; - details?: string | null; - url?: string | null; - application_id?: string | null; - assets?: { large_image?: string | null; small_image?: string | null }; - emoji?: { id?: string; animated?: boolean } | null; - app_icon?: string | null; -}; - -export type Data = { - discord_user: { - id: string; - avatar?: string | null; - discriminator?: string | null; - primary_guild?: { identity_guild_id?: string; badge?: string } | null; - avatar_decoration_data?: { asset?: string } | null; - }; - activities: Activity[]; - spotify?: { album_art_url?: string } | null; -}; - -export type ProfileSettings = { - optimized?: boolean; - animatedDecoration?: string | boolean; - ignoreAppId?: string[]; - theme?: string | null; -}; - -async function encodeBase64(url: string, _size?: ImageSize, _theme?: string | null) { - try { - const res = await fetch(url); - if (!res.ok) return url; - const buf = await res.arrayBuffer(); - const bytes = new Uint8Array(buf); - let binary = ""; - const chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)) as unknown as number[]); - } - const b64 = typeof globalThis.btoa === "function" ? globalThis.btoa(binary) : Buffer.from(binary, "binary").toString("base64"); - // try to infer mime - const ext = url.split("?")[0].split(".").pop()?.toLowerCase() ?? "png"; - const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/png"; - return `data:${mime};base64,${b64}`; - } catch (err) { - return url; - } -} - -export async function fetchUserImages(data: Data, settings: ProfileSettings) { - let avatar: string; - let avatarDecoration: string | null = null; - let clanBadge: string | null = null; - let assetLargeImage: string | null = null; - let assetSmallImage: string | null = null; - let userEmoji: string | null = null; - let albumCover: string | null = null; - - const avatarExtension = - data.discord_user.avatar && data.discord_user.avatar.startsWith("a_") && !settings.optimized - ? "gif" - : "webp"; - - const statusExtension: string = data.activities[0]?.emoji?.animated && !settings.optimized ? "gif" : "webp"; - - const userStatus: Activity | undefined = data.activities[0] && data.activities[0].type === 4 ? data.activities[0] : undefined; - - const activities = data.activities - .filter((activity) => activity.type === 0) - .filter((activity) => !settings.ignoreAppId?.includes(activity.application_id ?? "")); - const activity: Activity | undefined = activities.length > 0 ? activities[0] : undefined; - - if (data.discord_user.avatar) { - avatar = await encodeBase64( - `https://cdn.discordapp.com/avatars/${data.discord_user.id}/${data.discord_user.avatar}.${avatarExtension}?size=${avatarExtension === "gif" ? "64" : "256"}`, - ImageSize.USER_AVATAR - ); - } else { - avatar = await encodeBase64( - `https://cdn.discordapp.com/embed/avatars/${data.discord_user.discriminator === "0" ? Number(BigInt(data.discord_user.id) >> BigInt(22)) % 6 : Number(data.discord_user.discriminator ?? "0") % 5}.png?size=128`, - ImageSize.USER_AVATAR - ); - } - - if (data.discord_user.primary_guild && data.discord_user.primary_guild.identity_guild_id && data.discord_user.primary_guild.badge) { - clanBadge = await encodeBase64( - `https://cdn.discordapp.com/clan-badges/${data.discord_user.primary_guild.identity_guild_id}/${data.discord_user.primary_guild.badge}.png?size=32`, - ImageSize.SERVER_TAG - ); - } - - if (data.discord_user.avatar_decoration_data?.asset) { - avatarDecoration = await encodeBase64( - `https://cdn.discordapp.com/avatar-decoration-presets/${data.discord_user.avatar_decoration_data.asset}.png?size=64&passthrough=${settings.animatedDecoration || "false"}`, - ImageSize.USER_DECORATION - ); - } - - if (activity?.assets?.large_image) - assetLargeImage = await encodeBase64( - activity.assets?.large_image!.startsWith("mp:external/") - ? `https://media.discordapp.net/${activity.assets!.large_image!.replace("mp:", "")}` - : `https://cdn.discordapp.com/app-assets/${activity.application_id}/${activity.assets!.large_image}.webp`, - ImageSize.ACTIVITY_LARGE, - settings.theme || null - ); - else if (activity?.app_icon) - assetLargeImage = await encodeBase64(activity.app_icon, ImageSize.ACTIVITY_LARGE); - - if (activity?.assets?.small_image) - assetSmallImage = await encodeBase64( - activity.assets.small_image!.startsWith("mp:external/") - ? `https://media.discordapp.net/${activity.assets.small_image!.replace("mp:", "")}` - : `https://cdn.discordapp.com/app-assets/${activity.application_id}/${activity.assets.small_image}.webp`, - ImageSize.ACTIVITY_SMALL, - settings.theme || null - ); - - if (userStatus?.emoji?.id) - userEmoji = await encodeBase64( - `https://cdn.discordapp.com/emojis/${userStatus.emoji.id}.${statusExtension}?size=32`, - ImageSize.EMOJI - ); - - if (data.spotify?.album_art_url) albumCover = await encodeBase64(data.spotify.album_art_url, ImageSize.ACTIVITY_LARGE); - - return { - avatar, - clanBadge, - avatarDecoration, - assetLargeImage, - assetSmallImage, - userEmoji, - albumCover, - }; -} - -export default fetchUserImages; diff --git a/src/lib/discord/types.ts b/src/lib/discord/types.ts new file mode 100644 index 0000000..a5cf696 --- /dev/null +++ b/src/lib/discord/types.ts @@ -0,0 +1,35 @@ +export type DiscordStatus = 'online' | 'idle' | 'dnd' | 'offline'; + +export interface DiscordActivity { + name: string; + type: number; + details: string | null; + largeImage: string | null; + smallImage: string | null; +} + +export interface DiscordPresence { + status: DiscordStatus; + displayName: string | null; + avatarUrl: string | null; + guildTag: string | null; + guildTagBadgeUrl: string | null; + activities: DiscordActivity[]; +} + +export const offlinePresence: DiscordPresence = { + status: 'offline', + displayName: null, + avatarUrl: null, + guildTag: null, + guildTagBadgeUrl: null, + activities: [] +}; + +export const activityLabels: Record = { + 0: 'Playing', + 1: 'Streaming', + 2: 'Listening to', + 3: 'Watching', + 5: 'Competing in' +}; diff --git a/src/lib/discordbot.ts b/src/lib/discordbot.ts deleted file mode 100644 index b57e7d8..0000000 --- a/src/lib/discordbot.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Client, Partials, GatewayIntentBits, ActivityType } from "discord.js"; - -import { DISCORD_TOKEN, DISCORD_USER_ID, DISCORD_GUILD_ID } from "$env/static/private"; - -class Bot { - private client: Client; - - // Normalize a discord.js Activity object into a simple POJO - private async mapActivity(a: any) { - const type = typeof a.type === 'number' ? a.type : (ActivityType[a.type] ?? a.type); - const application_id = a.applicationId ?? a.application_id ?? null; - - const rawLarge = a.assets?.largeImage ?? a.assets?.large_image ?? null; - const rawSmall = a.assets?.smallImage ?? a.assets?.small_image ?? null; - - const formatAsset = (raw: string | null) => { - if (!raw) return null; - if (raw.startsWith('mp:external/')) { - return `https://media.discordapp.net/${raw.replace(/^mp:/, '')}`; - } - if (/^https?:\/\//.test(raw)) return raw; - if (application_id) return `https://cdn.discordapp.com/app-assets/${application_id}/${raw}.webp`; - return raw; - }; - - let app_icon = null; - if (type === 0 && application_id) { - try { - const appUser = await this.client.users.fetch(application_id); - app_icon = appUser.displayAvatarURL({ extension: 'webp', size: 128 }); - } catch (e) { - // Placeholder for apps that don't have a bot user - app_icon = "https://cdn.discordapp.com/embed/avatars/0.png"; - } - } - - return { - name: a.name ?? null, - type, - details: a.details ?? a.state ?? null, - url: a.url ?? null, - application_id, - assets: { - large_image: formatAsset(rawLarge), - small_image: formatAsset(rawSmall) - }, - app_icon, - emoji: a.emoji ? { id: a.emoji.id ?? null, animated: !!a.emoji.animated } : null - }; - } - - constructor() { - // Need presence and members intents to read user presence/activity - this.client = new Client({ - partials: [Partials.User], - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildPresences - ] - }); - - this.client.login(DISCORD_TOKEN).catch((error) => { - console.error("Failed to login to Discord:", error); - }); - - this.client.once("clientReady", async () => { - console.log(`Logged in as ${this.client.user?.tag}`); - }); - } - - getClient(): Client { - return this.client; - } - - /** - * Look up the user's presence specifically in the given guild. - * Returns null if guild/member/presence not available. - */ - async getUserActivityInGuild(guildId: string = DISCORD_GUILD_ID, userId: string = DISCORD_USER_ID) { - try { - // Fetch guild from cache or API - let guild = this.client.guilds.cache.get(guildId) || await this.client.guilds.fetch(guildId).catch(() => null); - if (!guild) return null; - - // Fetch member (will populate presence if available) - const member = await guild.members.fetch(userId).catch(() => null); - if (!member) return null; - - const presence = member.presence; - const user = member.user; - - // Build avatar URL - const avatarHash = user.avatar; - const avatarUrl = avatarHash - ? `https://cdn.discordapp.com/avatars/${user.id}/${avatarHash}.${avatarHash.startsWith('a_') ? 'gif' : 'webp'}?size=128` - : `https://cdn.discordapp.com/embed/avatars/${(BigInt(user.id) >> BigInt(22)) % BigInt(6)}.png`; - - // Get guild/clan tag if available - const guildTag = (user as any).primaryGuild?.tag ?? null; - const guildTagBadgeImage = `https://cdn.discordapp.com/clan-badges/${(user as any).primaryGuild?.identityGuildId}/${(user as any).primaryGuild?.badge}.png?size=32`; - - if (!presence) return { guildId, status: 'offline', activities: [], displayName: member.displayName, avatarUrl, guildTag, guildTagBadgeImage }; - - const status = presence.status; - const activities = await Promise.all((presence.activities || []).map(async (a: any) => await this.mapActivity(a))); - - return { guildId, status, activities, displayName: member.displayName, avatarUrl, guildTag, guildTagBadgeImage }; - } catch (err) { - return null; - } - } -} - -export const bot = new Bot(); - -// Re-export image helper for convenience -export { fetchUserImages } from "./discord/fetchUserImages"; \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts index a47847b..7b367d1 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,80 +1 @@ -// Central helper functions and re-exports -import { terminalSettings, pageSpeedSettings, speedPresets, pageAutoscrollSettings, user } from './config'; - -export type SpeedPreset = 'instant' | 'fast' | 'normal' | 'slow' | 'typewriter'; - -/** - * Calculate typing speed based on content length and speed multiplier - * Longer content types faster to maintain good UX - * @param contentLength - Length of the content to type - * @param speedMultiplier - Speed multiplier (0 = instant, 0.5 = 2x faster, 2 = 2x slower) - */ -export function calculateTypeSpeed(contentLength: number, speedMultiplier: number = 1): number { - // Instant mode - no delay - if (speedMultiplier === 0) return 0; - - const { baseTypeSpeed, minTypeSpeed, maxTypeSpeed } = terminalSettings; - - let speed: number; - - // Shorter content = slower typing (more readable) - // Longer content = faster typing (less waiting) - if (contentLength < 20) speed = maxTypeSpeed; - else if (contentLength < 50) speed = baseTypeSpeed; - else if (contentLength < 100) speed = baseTypeSpeed * 0.7; - else if (contentLength < 200) speed = baseTypeSpeed * 0.5; - else speed = minTypeSpeed; - - // Apply speed multiplier - return Math.max(0, Math.round(speed * speedMultiplier)); -} - -/** - * Get speed multiplier for a page - * @param pageName - Name of the page (e.g., 'home', 'portfolio') - */ -export function getPageSpeedMultiplier(pageName: string): number { - const setting = pageSpeedSettings[pageName]; - - if (setting === undefined) return 1; // Default to normal - - if (typeof setting === 'number') { - return setting; - } - - return speedPresets[setting] ?? 1; -} - -/** - * Get autoscroll setting for a page - * @param pageName - Name of the page (e.g., 'home', 'portfolio') - * @returns boolean - Whether autoscroll is enabled (default: true) - */ -export function getPageAutoscroll(pageName: string): boolean { - const setting = pageAutoscrollSettings[pageName]; - return setting ?? true; // Default to enabled -} - -/** - * Get the terminal prompt string - */ -export function getPrompt(path: string = '~'): string { - const { promptStyle } = terminalSettings; - - switch (promptStyle) { - case 'full': - return `${user.username}@${user.hostname}:${path}$`; - case 'short': - return `${user.username}:${path}$`; - case 'minimal': - return '$'; - default: - return '$'; - } -} - -// Barrel exports (convenience re-exports) -export { user, terminalSettings, pageSpeedSettings, speedPresets, pageAutoscrollSettings, pageMeta, site } from './config'; - -// Export terminal types -export type { TerminalAPI, TerminalLine, LineType, ParsedLine, DisplayedLine } from './components/tui/types'; +export * from './content'; diff --git a/src/lib/keybinds.ts b/src/lib/keybinds.ts deleted file mode 100644 index 1d1471e..0000000 --- a/src/lib/keybinds.ts +++ /dev/null @@ -1,113 +0,0 @@ -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+b', (e) => { - if (e.repeat) return; - tHeld = true; - tDigitUsed = false; - e.preventDefault(); - }); - - // macOS alias: option - hotkeys('option+b', (e) => { - if (e.repeat) return; - tHeld = true; - tDigitUsed = false; - e.preventDefault(); - }); - - // keyup: toggle if no digit used - hotkeys('alt+b', { 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+b', { 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+b'); - hotkeys.unbind('option+b'); - 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/models/model-scene.ts b/src/lib/models/model-scene.ts new file mode 100644 index 0000000..b7f99b4 --- /dev/null +++ b/src/lib/models/model-scene.ts @@ -0,0 +1,132 @@ +import * as THREE from 'three'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +const modelSize = 1.5; +const startingCameraPosition = new THREE.Vector3(0, 0.6, 2.5); + +export class ModelScene { + private scene = new THREE.Scene(); + private camera: THREE.PerspectiveCamera; + private renderer: THREE.WebGLRenderer; + private controls: OrbitControls; + private ambientLight = new THREE.AmbientLight(0xffffff, 0.6); + private model: THREE.Object3D | null = null; + private animationFrame = 0; + + constructor(private container: HTMLElement) { + const { clientWidth, clientHeight } = container; + this.camera = new THREE.PerspectiveCamera(45, clientWidth / clientHeight, 0.1, 1000); + this.camera.position.copy(startingCameraPosition); + + this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + this.renderer.setSize(clientWidth, clientHeight); + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + this.renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(this.renderer.domElement); + + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls.enableDamping = true; + this.controls.enablePan = false; + this.controls.minDistance = 1; + this.controls.maxDistance = 10; + this.controls.autoRotateSpeed = 2; + this.controls.listenToKeyEvents(container); + + this.addLights(); + this.animate(); + } + + private addLights() { + const keyLight = new THREE.DirectionalLight(0xffffff, 1); + keyLight.position.set(5, 5, 5); + const fillLight = new THREE.DirectionalLight(0xffffff, 0.5); + fillLight.position.set(-5, 3, -5); + const rimLight = new THREE.DirectionalLight(0x88ccff, 0.3); + rimLight.position.set(0, -3, -5); + this.scene.add(this.ambientLight, keyLight, fillLight, rimLight); + } + + private animate = () => { + this.animationFrame = requestAnimationFrame(this.animate); + this.controls.update(); + this.renderer.render(this.scene, this.camera); + }; + + load(url: string): Promise { + return new Promise((resolve, reject) => { + new GLTFLoader().load( + url, + (gltf) => { + this.removeModel(); + const model = gltf.scene; + const bounds = new THREE.Box3().setFromObject(model); + const center = bounds.getCenter(new THREE.Vector3()); + const largestSide = Math.max(...bounds.getSize(new THREE.Vector3()).toArray()); + const scale = modelSize / largestSide; + model.scale.setScalar(scale); + model.position.sub(center.multiplyScalar(scale)); + this.scene.add(model); + this.model = model; + this.resetCamera(); + resolve(); + }, + undefined, + reject + ); + }); + } + + private removeModel() { + if (!this.model) return; + this.scene.remove(this.model); + this.model.traverse((child) => { + if (!(child instanceof THREE.Mesh)) return; + child.geometry.dispose(); + const materials = Array.isArray(child.material) ? child.material : [child.material]; + materials.forEach((material) => material.dispose()); + }); + this.model = null; + } + + setAutoRotate(enabled: boolean) { + this.controls.autoRotate = enabled; + } + + setWireframe(enabled: boolean) { + this.model?.traverse((child) => { + if (!(child instanceof THREE.Mesh)) return; + const materials = Array.isArray(child.material) ? child.material : [child.material]; + materials.forEach((material) => { + if ('wireframe' in material) material.wireframe = enabled; + }); + }); + } + + setBrightness(intensity: number) { + this.ambientLight.intensity = intensity; + } + + resetCamera() { + this.camera.position.copy(startingCameraPosition); + this.controls.target.set(0, 0, 0); + this.controls.update(); + } + + resize() { + const { clientWidth, clientHeight } = this.container; + if (!clientWidth || !clientHeight) return; + this.camera.aspect = clientWidth / clientHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(clientWidth, clientHeight); + } + + dispose() { + cancelAnimationFrame(this.animationFrame); + this.removeModel(); + this.controls.dispose(); + this.renderer.dispose(); + this.renderer.domElement.remove(); + } +} diff --git a/src/lib/pages/about.ts b/src/lib/pages/about.ts deleted file mode 100644 index 174c406..0000000 --- a/src/lib/pages/about.ts +++ /dev/null @@ -1,279 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { user } from '$lib/config'; - -// const artist = [ -// { -// name: 'Noah Floersch', -// url: 'https://music.youtube.com/channel/UCgPjhKqPg5cPxErhqnoPlWg?si=B7GEz2QMNtrgnAbz' -// }, -// { -// name: 'Laufey', -// url: 'https://music.youtube.com/channel/UCJtROTPxo3qnEzww8JyDxuA?si=WwgYFkoccnXtJ6Im' -// }, -// { -// name: 'Tate McRae', -// url: 'https://music.youtube.com/channel/UCz86IA7ooUetFnUGa_YlsVw?si=A1t0-hLHswhbEG0U' -// } -// ] - -const hobbies = [ - { - name: 'Swimming', - description: 'I enjoy swimming as a way to relax and stay active. It is my favorite sport.', - icon: 'proicons:person-swim' - }, - { - name: 'Baking', - description: 'I love baking pastries and experimenting with new dessert recipes.', - icon: 'proicons:cake' - }, - { - name: 'Robotics', - description: 'Building and programming robots to solve problems is fun.', - icon: 'proicons:robot' - } -] - -const games = [ - { - name: 'Minecraft', - image: '/img/minecraft.png', - url: 'https://minecraft.net', - link: 'https://minecraft.net' - }, - { - name: 'Pokemon TCG Online', - image: '/img/pokemontcg.png', - url: 'https://www.pokemon.com/us/pokemon-tcg' - }, - { - name: "Fire Watch", - image: '/img/firewatch.png', - url: 'https://firewatchgame.com' - }, - { - name: "Expediton 33", - image: '/img/e33.png', - url: 'https://expedition33.com' - }, - { - name: "Mini Motorways", - image: '/img/mini.png', - url: 'https://store.steampowered.com/app/1127500/Mini_Motorways/' - }, - { - name: "Risk", - image: '/img/risk.png', - url: 'https://store.steampowered.com/app/1128810/RISK_Global_Domination/' - }, - { - name: "Slay the Spire", - image: '/img/slay_the_spire.png', - url: 'https://store.steampowered.com/app/646570/Slay_the_Spire/' - }, - { - name: "Slay the Spire 2", - image: '/img/slay_the_spire_2.png', - url: 'https://store.steampowered.com/app/2868840/Slay_the_Spire_2/' - } -] - -let shows = [ - { - name: 'Silo', - rating: '10/10', - imdburl: 'https://www.imdb.com/title/tt14688458/' - }, - { - name: 'The Mandalorian', - rating: '7/10', - imdburl: 'https://www.imdb.com/title/tt8111088/' - }, - { - name: "Andor", - rating: '9/10', - imdburl: 'https://www.imdb.com/title/tt9253284/' - }, - { - name: 'Severance', - rating: '9/10', - imdburl: 'https://www.imdb.com/title/tt11280740/' - }, - { - name: "D.P. (KDrama)", - rating: '10/10', - imdburl: 'https://www.imdb.com/title/tt13024974/' - } -]; - -shows = shows.sort((a, b) => { - const ratingA = parseFloat(a.rating.split('/')[0]); - const ratingB = parseFloat(b.rating.split('/')[0]); - return ratingB - ratingA; -}); - - -let movies = [ - { - name: 'Interstellar', - rating: '10/10', - imdburl: 'https://www.imdb.com/title/tt0816692/' - }, - { - name: "How to Train Your Dragon", - rating: '10/10', - imdburl: 'https://www.imdb.com/title/tt1621800/' - } -]; - -movies = movies.sort((a, b) => { - const ratingA = parseFloat(a.rating.split('/')[0]); - const ratingB = parseFloat(b.rating.split('/')[0]); - return ratingB - ratingA; -}); - - -export const lines: TerminalLine[] = [ - { type: 'command', content: 'cat ~/about.md' }, - { type: 'blank', content: '' }, - - { type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 200, inline: true }, - { - type: 'group', - content: '', - groupDirection: 'column', - groupAlign: 'start', - inline: true, - children: [ - { type: 'blank', content: '' }, - { type: 'header', content: `(&bold)${user.displayname}(&)` }, - { type: 'info', content: `(&accent)${user.title}(&)` }, - { type: 'output', content: `(&white)Location:(&) (&primary)${user.location}(&)` }, - { type: 'output', content: `(&muted)${user.bio}(&)` }, - ] - }, - { type: 'blank', content: '' }, - - { - type: 'group', content: '', groupAlign: 'start', groupGap: '1rem', - children: [ - { type: 'output', content: `(&primary, bold)Links >(&)`, inline: true }, - { type: 'link', href: "/about#hobbies", content: `(&bg-blue, black, bold)Hobbies(&)`, inline: true }, - { type: 'link', href: "/about#games", content: `(&bg-orange, black, bold)Games(&)`, inline: true }, - { type: 'link', href: "/about#shows", content: `(&bg-yellow, black, bold)Shows(&)`, inline: true }, - { type: 'link', href: "/about#movies", content: `(&bg-green, black, bold)Movies(&)`, inline: true }, - // { type: 'link', href: "/about#music", content: `(&bg-accent, black, bold)Music(&)`, inline: true }, - ] - }, - - - { type: 'divider', content: 'Hobbies', id: 'hobbies' }, - { - type: 'group', - content: '', - display: 'grid', - children: hobbies.map((h): TerminalLine => ({ - type: 'card', - cardTitle: h.name, - icon: h.icon, - content: h.description, - cardWidth: 1, - cardFloat: 'center', - })) - }, - { type: 'blank', content: '' }, - - { type: 'divider', content: 'Games', id: 'games' }, - { - type: 'group', - content: '', - display: 'grid', - children: games.map((g): TerminalLine => ({ - type: 'card', - cardTitle: g.name, - image: g.image, - imageAlt: g.name, - content: '', - cardWidth: 1, - cardFloat: 'center', - children: [ - { - type: 'button', - content: '(&icon, proicons:game) (&white)Play(&)', - href: g.url, - style: 'primary', - textStyle: 'center', - } - ] - })) - }, - - { type: 'blank', content: '' }, - - { type: 'divider', content: 'TV Shows', id: 'shows' }, - { - type: 'group', - content: '', - display: 'grid', - children: shows.map((s): TerminalLine => ({ - type: 'card', - cardTitle: s.name, - content: '', - cardWidth: 1, - cardFloat: 'center', - link: s.imdburl, - children: [ - { type: 'output', content: `Rating: (&accent)${s.rating}(&)` } - ] - })) - }, - - { type: 'blank', content: '' }, - - { type: 'divider', content: 'Movies', id: 'movies' }, - { - type: 'group', - content: '', - display: 'grid', - children: movies.map((m): TerminalLine => ({ - type: 'card', - cardTitle: m.name, - content: '', - cardWidth: 1, - cardFloat: 'center', - link: m.imdburl, - children: [ - { type: 'output', content: `Rating: (&accent)${m.rating}(&)` } - ] - })) - }, - - { type: 'blank', content: '' }, - - // { type: 'divider', content: 'Music Recommendations', id: 'music' }, - - // { - // type: 'group', - // content: '', - // groupDirection: 'row', - // children: [ - // { type: 'output', content: '(&accent)Artists:(&) ', inline: true }, - // ...artist.flatMap((a, i): TerminalLine[] => { - // const item: TerminalLine = { type: 'link', content: `(&primary)${a.name}(&)`, href: a.url, inline: true }; - // return i < artist.length - 1 ? [item, { type: 'output', content: ' (&muted)•(&) ', inline: true }] : [item]; - // }), - // ] - // }, - - // { type: 'blank', content: '' }, - // { - // type: 'card', - // id: 'music-player', - // content: '', - // mobile: false, - // children: [ - // { type: 'info', content: 'Loading playlist...' } - // ] - // }, - // { type: 'blank', content: '' } -]; diff --git a/src/lib/pages/components.ts b/src/lib/pages/components.ts deleted file mode 100644 index d90be31..0000000 --- a/src/lib/pages/components.ts +++ /dev/null @@ -1,900 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { user } from '$lib/config'; - -const sampleTableHeaders = ['Name', 'Type', 'Status']; -const sampleTableRows = [ - ['Button', 'Interactive', 'Ready'], - ['Progress', 'Display', 'Active'], - ['Card', 'Container', 'Ready'] -]; - -const sampleAccordionItems = [ - { title: 'What is this?', content: 'A showcase of all TUI components available in this terminal.' }, - { title: 'How to use?', content: 'Copy the code examples and customize for your needs.' }, - { title: 'Can I add more?', content: 'Yes! Check the types.ts file for the full API.' } -]; - -// Build comprehensive component showcase -export const lines: TerminalLine[] = [ - // Header - { type: 'command', content: 'cat ~/components/README.md' }, - { type: 'blank', content: '' }, - { type: 'header', content: '(&primary,bold)Terminal UI Components(&)' }, - { type: 'output', content: '(&muted)A comprehensive showcase of all available TUI components(&)' }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TEXT FORMATTING - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TEXT FORMATTING', id: 'text-formatting' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Colors(&)' }, - { type: 'output', content: '(&red)red(&) (&green)green(&) (&yellow)yellow(&) (&blue)blue(&) (&magenta)magenta(&) (&cyan)cyan(&) (&orange)orange(&) (&pink)pink(&)' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Semantic Colors(&)' }, - { type: 'output', content: '(&primary)primary(&) (&accent)accent(&) (&muted)muted(&) (&error)error(&) (&success)success(&) (&warning)warning(&) (&info)info(&)' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Text Styles(&)' }, - { type: 'output', content: '(&bold)bold(&) (&dim)dim(&) (&italic)italic(&) (&underline)underline(&) (&strikethrough)strikethrough(&)' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Combined Styles(&)' }, - { type: 'output', content: '(&bold,red)bold red(&) (&italic,cyan)italic cyan(&) (&bold,underline,yellow)bold underline yellow(&)' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Inline Icons(&)' }, - { type: 'output', content: '(&icon, mdi:github) GitHub (&icon, mdi:twitter) Twitter (&icon, mdi:heart) Love (&icon, mdi:star) Star' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Background Colors(&)' }, - { type: 'output', content: '(&bg-surface)surface bg(&) (&bg-red)red bg(&) (&bg-blue)blue bg(&) (&bg-green)green bg(&)' }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // LINE TYPES - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'LINE TYPES' }, - { type: 'blank', content: '' }, - - { type: 'command', content: 'echo "This is a command line"' }, - { type: 'output', content: 'This is an output line' }, - { type: 'info', content: 'This is an info line' }, - { type: 'success', content: 'This is a success line' }, - { type: 'warning', content: 'This is a warning line' }, - { type: 'error', content: 'This is an error line' }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // BUTTONS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'BUTTONS', id: 'buttons' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Button Styles(&)' }, - { - type: 'button', - content: 'Primary Button', - icon: 'mdi:check', - style: 'primary', - action: () => console.log('Primary clicked') - }, - { - type: 'button', - content: 'Secondary Button', - icon: 'mdi:information', - style: 'secondary', - action: () => console.log('Secondary clicked') - }, - { - type: 'button', - content: 'Accent Button', - icon: 'mdi:star', - style: 'accent', - action: () => console.log('Accent clicked') - }, - { - type: 'button', - content: 'Warning Button', - icon: 'mdi:alert', - style: 'warning', - action: () => console.log('Warning clicked') - }, - { - type: 'button', - content: 'Error Button', - icon: 'mdi:close-circle', - style: 'error', - action: () => console.log('Error clicked') - }, - { type: 'blank', content: '' }, - { - type: 'button', - content: 'Borderless Button', - icon: 'mdi:border-none', - style: 'primary', - border: false, - action: () => console.log('Borderless clicked') - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Link Buttons(&)' }, - { - type: 'button', - content: 'External Link (GitHub)', - icon: 'mdi:github', - style: 'primary', - href: 'https://github.com', - external: true - }, - { - type: 'button', - content: 'Internal Link (Home)', - icon: 'mdi:home', - style: 'accent', - href: '/' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // LINKS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'LINKS' }, - { type: 'blank', content: '' }, - - { - type: 'link', - content: 'Click here to visit GitHub', - href: 'https://github.com', - icon: 'mdi:github', - external: true - }, - { - type: 'link', - content: 'Go to portfolio page', - href: '/portfolio', - icon: 'mdi:folder' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // PROGRESS BARS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'PROGRESS BARS' }, - { type: 'blank', content: '' }, - - { - type: 'progress', - content: 'Loading...', - progress: 25, - progressLabel: 'Installing packages' - }, - { - type: 'progress', - content: '', - progress: 50, - progressLabel: 'Building project' - }, - { - type: 'progress', - content: '', - progress: 75, - progressLabel: 'Running tests' - }, - { - type: 'progress', - content: '', - progress: 100, - progressLabel: 'Complete!' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // GROUPS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'GROUPS' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Horizontal Group (Row)(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupAlign: 'center', - groupGap: '1rem', - children: [ - { type: 'output', content: '(&primary,bold)Status:(&)', inline: true }, - { type: 'success', content: '(&success)Online(&)', inline: true }, - { type: 'button', content: 'Refresh', icon: 'mdi:refresh', style: 'accent', inline: true, action: () => console.log('Refresh clicked') } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Vertical Group (Column)(&)' }, - { - type: 'group', - content: '', - groupDirection: 'column', - groupAlign: 'start', - groupGap: '0.25rem', - children: [ - { type: 'header', content: '(&accent,bold)User Profile(&)' }, - { type: 'output', content: '(&muted)Name:(&) John Doe' }, - { type: 'output', content: '(&muted)Role:(&) Developer' }, - { type: 'output', content: '(&muted)Status:(&) (&success)Active(&)' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Full Width Group(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupGap: '1rem', - groupExpand: true, - children: [ - { type: 'button', content: 'Full Width A', style: 'primary' }, - { type: 'button', content: 'Full Width B', style: 'accent' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Group with Links(&)' }, - { - type: 'group', - content: '', - groupAlign: 'start', - groupGap: '1rem', - children: [ - { type: 'output', content: '(&primary,bold)Quick Links:(&)', inline: true }, - { type: 'link', href: '#text-formatting', content: '(&bg-blue,black)Formatting(&)', inline: true }, - { type: 'link', href: '#buttons', content: '(&bg-green,black)Buttons(&)', inline: true }, - { type: 'link', href: '#terminal-api', content: '(&bg-orange,black)API(&)', inline: true } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Nested Groups(&)' }, - { - type: 'group', - content: '', - groupDirection: 'column', - groupGap: '0.5rem', - children: [ - { type: 'header', content: '(&cyan,bold)Settings Panel(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupGap: '1rem', - children: [ - { type: 'output', content: '(&muted)Theme:(&)', inline: true }, - { type: 'button', content: 'Dark', icon: 'mdi:weather-night', style: 'primary', inline: true, action: () => { } }, - { type: 'button', content: 'Light', icon: 'mdi:weather-sunny', style: 'accent', inline: true, action: () => { } } - ] - }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupGap: '1rem', - children: [ - { type: 'output', content: '(&muted)Speed:(&)', inline: true }, - { type: 'button', content: 'Slow', style: 'secondary', inline: true, action: () => { } }, - { type: 'button', content: 'Normal', style: 'primary', inline: true, action: () => { } }, - { type: 'button', content: 'Fast', style: 'accent', inline: true, action: () => { } } - ] - } - ] - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // INLINE COMPONENTS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'INLINE COMPONENTS' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Inline Cards(&)' }, - { type: 'output', content: '(&muted)Cards can be displayed inline for compact layouts:(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupAlign: 'start', - groupGap: '1rem', - children: [ - { - type: 'card', - content: 'Small card 1', - cardTitle: 'Card A', - inline: true - }, - { - type: 'card', - content: 'Small card 2', - cardTitle: 'Card B', - style: 'accent', - inline: true - }, - { - type: 'card', - content: 'Small card 3', - cardTitle: 'Card C', - style: 'warning', - inline: true - } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Inline Tables(&)' }, - { type: 'output', content: '(&muted)Tables can also be inline:(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupAlign: 'start', - groupGap: '1rem', - children: [ - { - type: 'table', - content: 'Server 1', - tableHeaders: ['Metric', 'Value'], - tableRows: [['CPU', '45%'], ['RAM', '2.4GB']], - inline: true - }, - { - type: 'table', - content: 'Server 2', - tableHeaders: ['Metric', 'Value'], - tableRows: [['CPU', '12%'], ['RAM', '1.1GB']], - inline: true - } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Inline Accordions(&)' }, - { - type: 'group', - content: '', - groupDirection: 'row', - groupAlign: 'start', - groupGap: '1rem', - children: [ - { - type: 'accordion', - content: 'Details A', - accordionItems: [{ title: 'More Info', content: 'Hidden details here.' }], - inline: true - }, - { - type: 'accordion', - content: 'Details B', - accordionItems: [{ title: 'More Info', content: 'Hidden details here.' }], - inline: true - } - ] - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // IMAGES - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'IMAGES' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Image with Caption(&)' }, - { - type: 'image', - content: 'User Avatar', - image: user.avatar, - imageAlt: 'Profile picture', - imageWidth: 100 - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // CARDS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'CARDS' }, - { type: 'blank', content: '' }, - - { - type: 'card', - content: 'This is card content. Cards can contain formatted text and are great for highlighting information.', - cardTitle: 'Card Title', - cardFooter: 'Card Footer' - }, - { type: 'blank', content: '' }, - { - type: 'card', - content: 'Cards can now include images and icons directly.', - cardTitle: 'Rich Card', - cardFooter: 'Updated Feature', - icon: 'mdi:star', - image: 'https://placehold.co/600x200/1e1e2e/cdd6f4?text=Card+Image', - imageAlt: 'Placeholder image', - children: [ - { - type: 'button', - content: 'View on GitHub', - href: 'https://github.com/adithyakrishnan2004', - icon: 'mdi:github', - style: 'primary' - } - ], - display: 'flex' - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Card Sizing & Floating(&)' }, - { - type: 'card', - content: 'Centered 50% width card', - cardTitle: 'Centered Card', - cardWidth: '1/2', - cardFloat: 'center', - style: 'primary' - }, - { - type: 'card', - content: 'Floated right 300px card', - cardTitle: 'Floated Card', - cardWidth: 300, - cardFloat: 'end', - style: 'accent' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TABLES - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TABLES' }, - { type: 'blank', content: '' }, - - { - type: 'table', - content: '', - tableHeaders: sampleTableHeaders, - tableRows: sampleTableRows - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // ACCORDIONS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'ACCORDIONS' }, - { type: 'blank', content: '' }, - - { - type: 'accordion', - content: '', - accordionItems: sampleAccordionItems, - accordionOpen: false - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TOOLTIPS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TOOLTIPS' }, - { type: 'blank', content: '' }, - - { - type: 'tooltip', - content: 'Hover over me for more info!', - tooltipText: 'This is tooltip content that appears on hover.', - tooltipPosition: 'top' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // DIVIDERS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'DIVIDER STYLES' }, - { type: 'blank', content: '' }, - - { type: 'output', content: '(&muted)Dividers are simple horizontal separators with optional text:(&)' }, - { type: 'divider', content: 'SECTION A' }, - { type: 'output', content: '(&muted)Content for section A...(&)' }, - { type: 'divider', content: 'SECTION B' }, - { type: 'output', content: '(&muted)Content for section B...(&)' }, - { type: 'divider', content: '' }, - { type: 'output', content: '(&muted)Empty divider above (no text)(&)' }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // FORM INPUTS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'FORM INPUTS' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Text Input(&)' }, - { - type: 'input', - content: 'Username:', - icon: 'mdi:account', - inputPlaceholder: 'Enter your username', - style: 'primary' - }, - { - type: 'input', - content: 'Email:', - icon: 'mdi:email', - inputPlaceholder: 'you@example.com', - inputType: 'email', - style: 'accent' - }, - { - type: 'input', - content: 'With prefix/suffix:', - inputPlaceholder: '100', - inputPrefix: '$', - inputSuffix: '.00', - inputType: 'number' - }, - { - type: 'input', - content: 'Error state:', - inputPlaceholder: 'Invalid input', - inputError: true, - inputErrorMessage: 'This field is required', - style: 'error' - }, - { - type: 'input', - content: 'Disabled:', - inputPlaceholder: 'Cannot edit', - inputDisabled: true - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TEXTAREA - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TEXTAREA' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Multi-line Text Area(&)' }, - { - type: 'textarea', - content: 'Message:', - icon: 'mdi:message-text', - inputPlaceholder: 'Type your message here...', - textareaRows: 4, - style: 'primary' - }, - { - type: 'textarea', - content: 'With character limit:', - inputPlaceholder: 'Limited to 100 characters', - textareaRows: 3, - textareaMaxLength: 100, - style: 'accent' - }, - { type: 'blank', content: '' }, - { - type: 'textarea', - content: 'Error state:', - inputPlaceholder: 'Invalid input...', - textareaRows: 2, - style: 'error', - inputError: true, - inputErrorMessage: 'Message is too short' - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // CHECKBOX - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'CHECKBOXES' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Checkbox Options(&)' }, - { - type: 'checkbox', - content: 'Enable notifications', - icon: 'mdi:bell', - style: 'primary' - }, - { - type: 'checkbox', - content: 'Accept terms and conditions', - style: 'accent' - }, - { - type: 'checkbox', - content: 'Indeterminate state', - checkboxIndeterminate: true, - style: 'warning' - }, - { - type: 'checkbox', - content: 'Disabled checkbox', - inputDisabled: true - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // RADIO BUTTONS - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'RADIO BUTTONS' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Radio Group (Vertical)(&)' }, - { - type: 'radio', - content: 'Select theme:', - icon: 'mdi:palette', - style: 'primary', - radioOptions: [ - { value: 'dark', label: 'Dark Mode', icon: 'mdi:weather-night' }, - { value: 'light', label: 'Light Mode', icon: 'mdi:weather-sunny' }, - { value: 'system', label: 'System Default', icon: 'mdi:desktop-mac' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Radio Group (Horizontal)(&)' }, - { - type: 'radio', - content: 'Size:', - style: 'accent', - radioHorizontal: true, - radioOptions: [ - { value: 'sm', label: 'Small' }, - { value: 'md', label: 'Medium' }, - { value: 'lg', label: 'Large' }, - { value: 'xl', label: 'Extra Large', disabled: true } - ] - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // SELECT DROPDOWN - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'SELECT DROPDOWN' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Basic Select(&)' }, - { - type: 'select', - content: 'Country:', - icon: 'mdi:earth', - inputPlaceholder: 'Select a country...', - style: 'primary', - selectOptions: [ - { value: 'us', label: 'United States', icon: 'emojione-v1:flag-for-united-states' }, - { value: 'uk', label: 'United Kingdom', icon: 'emojione-v1:flag-for-united-kingdom' }, - { value: 'de', label: 'Germany', icon: 'emojione-v1:flag-for-germany' }, - { value: 'fr', label: 'France', icon: 'emojione-v1:flag-for-france' }, - { value: 'jp', label: 'Japan', icon: 'emojione-v1:flag-for-japan' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Searchable Select(&)' }, - { - type: 'select', - content: 'Programming Language:', - icon: 'mdi:code-braces', - inputPlaceholder: 'Search languages...', - style: 'accent', - selectSearchable: true, - selectOptions: [ - { value: 'ts', label: 'TypeScript', icon: 'mdi:language-typescript' }, - { value: 'js', label: 'JavaScript', icon: 'mdi:language-javascript' }, - { value: 'py', label: 'Python', icon: 'mdi:language-python' }, - { value: 'rs', label: 'Rust', icon: 'mdi:language-rust' }, - { value: 'go', label: 'Go', icon: 'mdi:language-go' }, - { value: 'cpp', label: 'C++', icon: 'mdi:language-cpp' }, - { value: 'java', label: 'Java', icon: 'mdi:language-java' } - ] - }, - { type: 'blank', content: '' }, - { - type: 'select', - content: 'Error state:', - inputPlaceholder: 'Select something...', - style: 'error', - inputError: true, - inputErrorMessage: 'Selection required', - selectOptions: [ - { value: '1', label: 'Option 1' } - ] - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TOGGLE SWITCH - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TOGGLE SWITCHES' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Toggle Options(&)' }, - { - type: 'toggle', - content: 'Dark Mode', - icon: 'mdi:theme-light-dark', - style: 'primary' - }, - { - type: 'toggle', - content: 'Airplane Mode', - icon: 'mdi:airplane', - style: 'accent', - toggleOnLabel: 'ON', - toggleOffLabel: 'OFF' - }, - { - type: 'toggle', - content: 'Custom Labels', - icon: 'mdi:toggle-switch', - style: 'warning', - toggleOnLabel: 'YES', - toggleOffLabel: 'NO' - }, - { - type: 'toggle', - content: 'No Labels', - toggleShowLabels: false, - style: 'accent' - }, - { - type: 'toggle', - content: 'Disabled Toggle', - inputDisabled: true - }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // USAGE EXAMPLES - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'USAGE' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Code Example(&)' }, - { type: 'output', content: "(&muted)// Text formatting(&)" }, - { type: 'output', content: "{ type: 'output', content: '(&green)colored text(&)' }" }, - { type: 'blank', content: '' }, - { type: 'output', content: "(&muted)// Button with action(&)" }, - { type: 'output', content: "{ type: 'button', content: 'Click', icon: 'mdi:check', style: 'primary', action: () => {} }" }, - { type: 'blank', content: '' }, - { type: 'output', content: "(&muted)// Form input(&)" }, - { type: 'output', content: "{ type: 'input', content: 'Label:', inputPlaceholder: 'Enter value...', style: 'primary' }" }, - { type: 'blank', content: '' }, - { type: 'output', content: "(&muted)// Checkbox(&)" }, - { type: 'output', content: "{ type: 'checkbox', content: 'Enable option', style: 'accent' }" }, - { type: 'blank', content: '' }, - { type: 'output', content: "(&muted)// Group with children(&)" }, - { type: 'output', content: "{ type: 'group', groupDirection: 'row', groupGap: '1rem', children: [...] }" }, - { type: 'blank', content: '' }, - { type: 'output', content: "(&muted)// Select dropdown(&)" }, - { type: 'output', content: "{ type: 'select', content: 'Choose:', selectOptions: [{ value: 'a', label: 'Option A' }] }" }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // TERMINAL API - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'TERMINAL API', id: 'terminal-api' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Reactive Terminal Control(&)' }, - { type: 'output', content: 'The (&cyan)TerminalAPI(&) allows programmatic control of terminal content.' }, - { type: 'output', content: 'Use (&primary)bind:terminal(&) to access the API from your component.' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Writing Lines(&)' }, - { type: 'output', content: "(&muted)terminal.write({ type: 'output', content: 'Hello!' })(&) (&dim)// Append line(&)" }, - { type: 'output', content: "(&muted)terminal.writeLines([...lines])(&) (&dim)// Append multiple(&)" }, - { type: 'output', content: "(&muted)terminal.clear()(&) (&dim)// Clear all(&)" }, - { type: 'output', content: "(&muted)terminal.setLines([...lines])(&) (&dim)// Replace all(&)" }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Updating Lines(&)' }, - { type: 'output', content: "(&muted)terminal.update(0, { content: 'New text' })(&) (&dim)// Update by index(&)" }, - { type: 'output', content: "(&muted)terminal.updateContent(0, 'New text')(&) (&dim)// Update content only(&)" }, - { type: 'output', content: "(&muted)terminal.updateById('my-id', { content: 'New' })(&) (&dim)// Update by ID(&)" }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Removing Lines(&)' }, - { type: 'output', content: "(&muted)terminal.remove(0)(&) (&dim)// Remove by index(&)" }, - { type: 'output', content: "(&muted)terminal.removeRange(0, 3)(&) (&dim)// Remove range(&)" }, - { type: 'output', content: "(&muted)terminal.removeById('my-id')(&) (&dim)// Remove by ID(&)" }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Navigation & Control(&)' }, - { type: 'output', content: "(&muted)terminal.scrollToBottom()(&) (&dim)// Scroll to end(&)" }, - { type: 'output', content: "(&muted)terminal.scrollToLine(5)(&) (&dim)// Scroll to line(&)" }, - { type: 'output', content: "(&muted)terminal.skip()(&) (&dim)// Skip animation(&)" }, - { type: 'output', content: "(&muted)terminal.restart()(&) (&dim)// Restart animation(&)" }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Reading State(&)' }, - { type: 'output', content: "(&muted)terminal.getLine(0)(&) (&dim)// Get line by index(&)" }, - { type: 'output', content: "(&muted)terminal.getLines()(&) (&dim)// Get all lines(&)" }, - { type: 'output', content: "(&muted)terminal.getLineCount()(&) (&dim)// Get count(&)" }, - { type: 'output', content: "(&muted)terminal.findById('my-id')(&) (&dim)// Find index by ID(&)" }, - { type: 'output', content: "(&muted)terminal.isAnimating()(&) (&dim)// Check if typing(&)" }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Usage Example(&)' }, - { type: 'output', content: "(&dim)// In your Svelte component:(&)" }, - { type: 'output', content: "(&cyan)import(&) TerminalTUI (&cyan)from(&) '(&green)$lib/components/TerminalTUI.svelte(&)';" }, - { type: 'output', content: "(&cyan)import type(&) { TerminalAPI } (&cyan)from(&) '(&green)$lib(&)';" }, - { type: 'output', content: "" }, - { type: 'output', content: "(&magenta)let(&) terminal = (&yellow)$state(&)();" }, - { type: 'output', content: "(&magenta)let(&) lines = (&yellow)$state(&)([{ type: '(&green)output(&)', content: '(&green)Hello!(&)' }]);" }, - { type: 'output', content: "" }, - { type: 'output', content: "(&dim)// In template:(&)" }, - { type: 'output', content: "" }, - { type: 'output', content: "" }, - { type: 'output', content: "(&dim)// Then use the API:(&)" }, - { type: 'output', content: "terminal?.write({ type: '(&green)success(&)', content: '(&green)Done!(&)' });" }, - { type: 'blank', content: '' }, - - // ═══════════════════════════════════════════════════════════════ - // DISPLAY & NESTING - // ═══════════════════════════════════════════════════════════════ - { type: 'divider', content: 'DISPLAY & NESTING' }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Inline Grouping Logic(&)' }, - { type: 'output', content: '(&muted)Consecutive inline elements are grouped. Non-consecutive are separated.(&)' }, - { type: 'output', content: '(&muted)1(&)', inline: true }, - { type: 'output', content: '(&muted)2(&)', inline: true }, - { type: 'blank', content: '' }, - { type: 'output', content: '(&muted)3(&)', inline: true }, - { type: 'output', content: '(&muted)4(&)', inline: true }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Card with Nested Children (Grid)(&)' }, - { - type: 'card', - content: 'This card contains other cards in a grid layout.', - cardTitle: 'Parent Card (Grid)', - display: 'grid', - children: [ - { type: 'card', content: 'Child 1', cardTitle: 'C1' }, - { type: 'card', content: 'Child 2', cardTitle: 'C2' }, - { type: 'card', content: 'Child 3', cardTitle: 'C3' }, - { type: 'card', content: 'Child 4', cardTitle: 'C4' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Accordion with Nested Children (Flex)(&)' }, - { - type: 'accordion', - content: 'Open to see nested items', - display: 'flex', - children: [ - { type: 'button', content: 'Action 1', style: 'primary', icon: 'mdi:check' }, - { type: 'button', content: 'Action 2', style: 'secondary', icon: 'mdi:close' }, - { type: 'button', content: 'Action 3', style: 'accent', icon: 'mdi:star' } - ] - }, - { type: 'blank', content: '' }, - - { type: 'info', content: '(&blue,bold)Group with Grid Layout(&)' }, - { - type: 'group', - content: '', - display: 'grid', - groupGap: '1rem', - children: [ - { type: 'card', content: 'Grid Item 1', cardTitle: 'G1' }, - { type: 'card', content: 'Grid Item 2', cardTitle: 'G2' }, - { type: 'card', content: 'Grid Item 3', cardTitle: 'G3' } - ] - }, - { type: 'blank', content: '' }, - - // End - { type: 'success', content: '(&success)Component showcase complete!(&)' } -]; \ No newline at end of file diff --git a/src/lib/pages/hackathons.ts b/src/lib/pages/hackathons.ts deleted file mode 100644 index de713a9..0000000 --- a/src/lib/pages/hackathons.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { sortedCards } from '$lib/config'; -import { projectNavLinks } from './projectNav'; -import { tagColor } from './palette'; - -const totalHackathons = sortedCards.length; -const totalAwards = sortedCards.filter(c => c.awards && c.awards.length > 0).length; -const featuredCount = sortedCards.filter(c => c.featured).length; - -const hackathonSections: TerminalLine[] = sortedCards.flatMap(card => { - const hasImage = !!card.image; - const hasAwards = !!card.awards?.length; - const icon = hasAwards ? 'mdi:trophy' : 'mdi:rocket-launch'; - - const infoChildren: TerminalLine[] = [ - { - type: 'output' as const, - content: `(&icon, ${icon}) (&bg-green,black) HACKATHON (&)${card.featured ? ' (&bg-orange,black) FEATURED (&)' : ''}` - }, - ...(card.hackathonName - ? [{ type: 'info' as const, content: `(&primary)${card.hackathonName}(&)${card.year ? ` (&muted)(${card.year})(&)` : ''}` }] - : []), - ...(card.university - ? [{ type: 'output' as const, content: `(&muted)${card.university}${card.location ? `, ${card.location}` : ''}(&)` }] - : []), - { type: 'output' as const, content: `(&text)${card.description}(&)` }, - ...(card.awards?.length - ? card.awards.map(award => ({ type: 'success' as const, content: `(&success)🏆 ${award.place} — ${award.track}(&)` })) - : []), - ...(card.tags?.length - ? [{ type: 'output' as const, content: card.tags.map((tag, i) => `(&bg-${tagColor(i)},black) ${tag} (&)`).join(' ') }] - : []), - ...(card.liveWarning ? [{ type: 'warning' as const, content: `(&warning)Demo may be unavailable(&)` }] : []), - { - type: 'group' as const, - content: '', - groupDirection: 'row' as const, - groupGap: '0.75rem', - groupExpand: true, - children: [ - ...(card.link - ? [{ type: 'button' as const, content: 'View Demo', href: card.link, icon: 'mdi:open-in-new', style: 'primary' as const, flex: true }] - : []), - ...(card.repo - ? [{ type: 'button' as const, content: 'View Code', href: card.repo, icon: 'mdi:github', style: 'accent' as const, flex: true }] - : []), - ...(card.devpost - ? [{ type: 'button' as const, content: 'View on Devpost', href: card.devpost, icon: 'mdi:rocket-launch', style: 'secondary' as const, flex: true }] - : []) - ] - } - ]; - - return [ - { type: 'divider' as const, content: card.title.toUpperCase() }, - ...(hasImage - ? [{ type: 'image' as const, content: '', image: card.image as string, imageAlt: card.title, imageWidth: 350, inline: true }] - : []), - { type: 'group' as const, content: '', inline: hasImage, groupDirection: 'column' as const, groupAlign: 'stretch' as const, children: infoChildren }, - { type: 'blank' as const, content: '' } - ]; -}); - -export const lines: TerminalLine[] = [ - { type: 'command' as const, content: 'ls ~/projects/hackathons' }, - { type: 'blank' as const, content: '', height: '1rem' }, - projectNavLinks, - { type: 'blank' as const, content: '', height: '1rem' }, - { type: 'header' as const, content: 'Hackathon Journey' }, - { type: 'output' as const, content: `(&muted)Total:(&) (&primary)${totalHackathons}(&) (&muted)| Awards:(&) (&yellow)${totalAwards}(&) (&muted)| Featured:(&) (&accent)${featuredCount}(&)` }, - { type: 'blank' as const, content: '' }, - ...hackathonSections -]; diff --git a/src/lib/pages/home.ts b/src/lib/pages/home.ts deleted file mode 100644 index 4482d95..0000000 --- a/src/lib/pages/home.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 - { 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)███(&)', inline: true }, - { type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)', inline: true }, - - { type: 'blank', content: '' }, - { type: 'header', content: `HI, I'm ${user.displayname}` }, - { type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: true }, - - { type: 'group', content: '', inline: true, groupDirection: 'column', children: [ - { type: 'output', content: `(&text)${user.bio}(&)` }, - { type: 'group', content: '', groupDirection: 'row', children: [ - // Interactive navigation buttons - ...navigation.map((nav, i) => ({ - type: 'button' as const, - content: nav.name + ` (&text, bold)[${i+1}](&)`, - icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy', - style: 'primary' as const, - href: nav.path, - inline: true - })), - ] - } - ] - }, - - { type: 'divider', content: 'Website Keybinds' }, - { type: 'output', content: '(&orange)Toggle Light/Dark Mode(&) (&text, bold)(Alt/Option+B)(&)' , inline: true }, - { type: 'output', content: '(&muted)•(&) (&orange)Select Theme(&) (&text, bold)(Alt/Option+T + [#])(&)' , inline: true }, - { type: 'output', content: '(&muted)•(&) (&orange)Skip typing animation(&) (&text, bold)(Y)(&)' , inline: true }, - { type: 'output', content: '(&muted)•(&) (&orange)Navigate to page(&) (&text, bold)(Ctrl/Cmd+ [#])(&)' , inline: true }, - { type: 'blank', content: '' }, - // list the themes - { type: 'output', content: '(&accent)Themes:(&)', inline: true }, - ...themeOptions.map((theme, i) => ( - { - type: 'output' as const, - content: `(&primary)${theme.label}(&) (&text, bold)[${i+1}](&)`, - inline: true - } - )), - { type: 'blank', content: '' }, -]; \ No newline at end of file diff --git a/src/lib/pages/opensource.ts b/src/lib/pages/opensource.ts deleted file mode 100644 index 6185ced..0000000 --- a/src/lib/pages/opensource.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { openSourceProjects } from '$lib/config'; -import { projectNavLinks } from './projectNav'; -import { tagColor } from './palette'; - -const projectSections: TerminalLine[] = openSourceProjects.flatMap(project => { - const hasImage = !!project.image; - - const infoChildren: TerminalLine[] = [ - { type: 'output' as const, content: `(&icon, mdi:source-branch) (&bg-blue,black) OPEN SOURCE (&)` }, - { type: 'output' as const, content: `(&text)${project.description}(&)` }, - { - type: 'output' as const, - content: project.tech.map((tech, i) => `(&bg-${tagColor(i)},black) ${tech} (&)`).join(' ') - }, - { - type: 'group' as const, - content: '', - groupDirection: 'row' as const, - groupGap: '0.75rem', - groupExpand: true, - children: [ - ...(project.github - ? [{ type: 'button' as const, content: 'View on GitHub', href: project.github, icon: 'mdi:github', style: 'accent' as const, flex: true }] - : []), - ...(project.live - ? [{ type: 'button' as const, content: 'View Live Demo', href: project.live, icon: 'mdi:open-in-new', style: 'primary' as const, flex: true }] - : []) - ] - } - ]; - - return [ - { type: 'divider' as const, content: project.name.toUpperCase() }, - ...(hasImage - ? [{ type: 'image' as const, content: '', image: project.image as string, imageAlt: project.name, imageWidth: 350, inline: true }] - : []), - { type: 'group' as const, content: '', inline: hasImage, groupDirection: 'column' as const, groupAlign: 'stretch' as const, children: infoChildren }, - { type: 'blank' as const, content: '' } - ]; -}); - -export const lines: TerminalLine[] = [ - { type: 'command' as const, content: 'ls ~/projects/opensource' }, - { type: 'blank' as const, content: '', height: '1rem' }, - projectNavLinks, - { type: 'blank' as const, content: '', height: '1rem' }, - { type: 'header' as const, content: 'Open Source' }, - { type: 'output' as const, content: `(&muted)Libraries and tools I build and maintain in the open.(&)` }, - { type: 'blank' as const, content: '' }, - ...projectSections -]; diff --git a/src/lib/pages/packages.ts b/src/lib/pages/packages.ts deleted file mode 100644 index df04753..0000000 --- a/src/lib/pages/packages.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { packageProjects } from '$lib/config'; -import { projectNavLinks } from './projectNav'; -import { tagColor } from './palette'; - -const orderedPackages = [...packageProjects].sort((a, b) => Number(b.featured) - Number(a.featured)); - -const packageSections: TerminalLine[] = orderedPackages.flatMap(pkg => { - const infoChildren: TerminalLine[] = [ - { - type: 'output' as const, - content: `(&icon, mdi:package-variant, 32) (&bg-orange,black) PACKAGE (&)${pkg.featured ? ' (&bg-accent,black) FEATURED (&)' : ''}` - }, - { type: 'output' as const, content: `(&text)${pkg.description}(&)` }, - { - type: 'output' as const, - content: pkg.tech.map((tech, i) => `(&bg-${tagColor(i)},black) ${tech} (&)`).join(' ') - }, - { - type: 'group' as const, - content: '', - groupDirection: 'row' as const, - groupGap: '0.75rem', - groupExpand: true, - children: [ - ...(pkg.github - ? [{ type: 'button' as const, content: 'View on GitHub', href: pkg.github, icon: 'mdi:github', style: 'accent' as const, flex: true }] - : []), - ...(pkg.live - ? [{ type: 'button' as const, content: 'View Package', href: pkg.live, icon: 'mdi:package-variant', style: 'primary' as const, flex: true }] - : []) - ] - } - ]; - - return [ - { type: 'divider' as const, content: pkg.name.toUpperCase() }, - { type: 'group' as const, content: '', groupDirection: 'column' as const, groupAlign: 'stretch' as const, children: infoChildren }, - { type: 'blank' as const, content: '' } - ]; -}); - -export const lines: TerminalLine[] = [ - { type: 'command' as const, content: 'ls ~/projects/packages' }, - { type: 'blank' as const, content: '', height: '1rem' }, - projectNavLinks, - { type: 'blank' as const, content: '', height: '1rem' }, - { type: 'header' as const, content: 'Packages' }, - { type: 'output' as const, content: `(&muted)Published packages and CLI tools you can install directly.(&)` }, - { type: 'blank' as const, content: '' }, - ...packageSections -]; diff --git a/src/lib/pages/palette.ts b/src/lib/pages/palette.ts deleted file mode 100644 index b1fc052..0000000 --- a/src/lib/pages/palette.ts +++ /dev/null @@ -1,5 +0,0 @@ -const tagPalette = ['blue', 'magenta', 'cyan', 'orange', 'green', 'pink']; - -export function tagColor(index: number): string { - return tagPalette[index % tagPalette.length]; -} diff --git a/src/lib/pages/portfolio.ts b/src/lib/pages/portfolio.ts deleted file mode 100644 index 11390bc..0000000 --- a/src/lib/pages/portfolio.ts +++ /dev/null @@ -1,129 +0,0 @@ - -import type { TerminalLine } from '$lib/components/tui/types'; -import { user, skills, openSourceProjects, experience } from '$lib/config'; - -const experienceElements: TerminalLine[] = []; - -experience.forEach(exp => { - experienceElements.push( - { type: 'header', content: `(&orange,bold)${exp.role}(&)` }, - { type: 'output', content: `(&primary)${exp.organization}(&)` + (exp.employmentType ? ` (&muted)· ${exp.employmentType}(&)` : '') }, - { type: 'output', content: `(&muted)${exp.period}(&)` + (exp.location ? ` (&muted)· ${exp.location}(&)` : '') }, - ...(exp.description ? [{ type: 'output' as const, content: `(&muted)${exp.description}(&)` }] : []), - ...(exp.tech ? [{ type: 'output' as const, content: ' ' + exp.tech.map(t => `(&blue)${t}(&)`).join(' (&muted)•(&) ') }] : []), - ...(exp.awards ? exp.awards.map(award => ({ type: 'success' as const, content: `(&success)${award}(&)` })) : []), - { type: 'blank', content: '' } - ); -}); - -const openSourceElements:TerminalLine[] = []; - -openSourceProjects.forEach(project => { - openSourceElements.push( - { - type: 'output', - content: `(&orange,bold)▸ ${project.name}(&) - ${project.description}` - }, - { - type: 'output', - content: ` ${project.tech.map(tech => `(&blue)${tech}(&)`).join(' (&muted)•(&) ')}` - }, - { type: 'blank', content: '' } - ); -}); - -export const lines: TerminalLine[] = [ - // Header command - { type: 'command', content: 'cat ~/portfolio.md' }, - { type: 'blank', content: '' }, - - // Avatar image - { type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 180, inline: true }, - - { - type: 'group', - content: '', - groupDirection: 'column', - groupAlign: 'start', - inline: true, - children: [ - // User info - { type: 'header', content: `(&bold)${user.name}(&)` }, - { type: 'info', content: `(&accent)B.S. Computer Science Student(&)` }, - { type: 'output', content: `(&white)Location:(&) (&primary)${user.location}(&)` }, - { type: 'output', content: `(&muted)My passion is using Computer Science with practical Engineering to enhance technology for the future.(&)` }, - ] - }, - - { type: 'blank', content: '' }, - { type: 'blank', content: '' }, - { - type: 'group', content: '', groupAlign: 'start', groupGap: '1rem', - children: [ - { type: 'output', content: `(&primary, bold)Links >(&)`, inline: true }, - { type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true }, - { type: 'link', href: "/portfolio#experience", content: `(&bg-magenta,black)Experience(&)`, 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: 'divider', content: 'CONTACT', id: 'contact' }, - // Contact buttons - dynamically generated from socials array - ...user.socials.map(social => ({ - type: 'button' as const, - content: `${social.name}`, - icon: social.icon, - style: 'primary' as const, - href: social.link, - inline: true - })), - - { type: 'divider', content: 'EXPERIENCE', id: 'experience' }, - - ...experienceElements, - - { type: 'divider', content: 'SKILLS', id: 'skills' }, - - // Skills as TUI sections - - // Languages - { type: 'info', content: '(&blue,bold)▸ Languages(&)' }, - { type: 'output', content: ' ' + skills.languages.map(s => `(&cyan)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Frameworks - { type: 'info', content: '(&magenta,bold)▸ Frameworks(&)' }, - { type: 'output', content: ' ' + skills.frameworks.map(s => `(&pink)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Applications - { type: 'info', content: '(&green,bold)▸ Tools(&)' }, - { type: 'output', content: ' ' + skills.tools.map(s => `(&green)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Platforms - { type: 'info', content: '(&yellow,bold)▸ Platforms(&)' }, - { type: 'output', content: ' ' + skills.platforms.map(s => `(&yellow)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Applications - { type: 'info', content: '(&purple,bold)▸ Applications(&)' }, - { type: 'output', content: ' ' + skills.applications.map(s => `(&purple)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Databases - { type: 'info', content: '(&cyan,bold)▸ Databases(&)' }, - { type: 'output', content: ' ' + skills.databases.map(s => `(&cyan)${s}(&)`).join(' (&muted)•(&) ') }, - { type: 'blank', content: '' }, - - // Interests - { type: 'info', content: '(&accent,bold)▸ Interests(&)' }, - { type: 'output', content: ' ' + skills.interests.map(s => `(&muted)${s}(&)`).join(' (&muted)•(&) ') }, - - { type: 'divider', content: 'PROJECTS', id: 'projects' }, - - ...openSourceElements, - - // End - { type: 'success', content: `(&success)Portfolio loaded successfully!(&)` } -]; \ No newline at end of file diff --git a/src/lib/pages/projectNav.ts b/src/lib/pages/projectNav.ts deleted file mode 100644 index 500c145..0000000 --- a/src/lib/pages/projectNav.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; - -const spacer: TerminalLine = { type: 'output' as const, content: '  ', inline: true }; - -export const projectNavLinks: TerminalLine = { - type: 'group' as const, - content: '', - groupAlign: 'start', - groupGap: '1rem', - children: [ - { type: 'link' as const, href: '/projects', content: `(&bg-primary,black) ← Projects (&)`, inline: true }, - spacer, - { type: 'link' as const, href: '/projects/opensource', content: `(&bg-pink,black) Open Source (&)`, inline: true }, - spacer, - { type: 'link' as const, href: '/projects/packages', content: `(&bg-orange,black) Packages (&)`, inline: true }, - spacer, - { type: 'link' as const, href: '/projects/hackathons', content: `(&bg-green,black) Hackathons (&)`, inline: true }, - spacer, - { type: 'link' as const, href: '/projects/research', content: `(&bg-cyan,black) Research (&)`, inline: true } - ] -}; diff --git a/src/lib/pages/projects.ts b/src/lib/pages/projects.ts deleted file mode 100644 index d5f3681..0000000 --- a/src/lib/pages/projects.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { sortedCards, packageProjects, openSourceProjects, researchItems } from '$lib/config'; - -const totalHackathons = sortedCards.length; -const totalAwards = sortedCards.filter(c => c.awards && c.awards.length > 0).length; -const cardWidth = 'calc((100% - 1.5rem) / 4)'; - -export const lines: TerminalLine[] = [ - { type: 'command' as const, content: 'ls ~/projects' }, - { type: 'header' as const, content: 'My Project Universe' }, - { type: 'output' as const, content: `(&muted)Everything I build lives in one of four buckets. Pick a door.(&)` }, - { type: 'blank' as const, content: '' }, - { type: 'output' as const, content: `(&muted)Open Source:(&) (&primary)${openSourceProjects.length}(&) (&muted)Packages:(&) (&orange)${packageProjects.length}(&) (&muted)Hackathons:(&) (&green)${totalHackathons}(&) (&muted)(${totalAwards} with awards)(&) (&muted)Research:(&) (&cyan)${researchItems.length}(&)` }, - { type: 'blank' as const, content: '' }, - { - type: 'card' as const, - content: '', - cardTitle: 'Open Source', - icon: 'mdi:source-branch', - cardFooter: `${openSourceProjects.length} projects`, - children: [ - { type: 'output' as const, content: `(&muted)Libraries and tools I build and maintain in the open.(&)` }, - { type: 'button' as const, content: 'Browse Open Source', href: '/projects/opensource', icon: 'mdi:arrow-right', style: 'accent' as const } - ], - inline: true, - display: 'flex' as const, - cardWidth - }, - { - type: 'card' as const, - content: '', - cardTitle: 'Packages', - icon: 'mdi:package-variant', - cardFooter: `${packageProjects.length} published`, - children: [ - { type: 'output' as const, content: `(&muted)CLI tools and packages published for others to install.(&)` }, - { type: 'button' as const, content: 'Browse Packages', href: '/projects/packages', icon: 'mdi:arrow-right', style: 'accent' as const } - ], - inline: true, - display: 'flex' as const, - cardWidth - }, - { - type: 'card' as const, - content: '', - cardTitle: 'Hackathons', - icon: 'mdi:trophy', - cardFooter: `${totalHackathons} events · ${totalAwards} awards`, - children: [ - { type: 'output' as const, content: `(&muted)Weekend builds, demos, and the awards they picked up.(&)` }, - { type: 'button' as const, content: 'Browse Hackathons', href: '/projects/hackathons', icon: 'mdi:arrow-right', style: 'accent' as const } - ], - inline: true, - display: 'flex' as const, - cardWidth - }, - { - type: 'card' as const, - content: '', - cardTitle: 'Research', - icon: 'mdi:microscope', - cardFooter: `${researchItems.length} items`, - children: [ - { type: 'output' as const, content: `(&muted)My Research Roles, Projects, and Publications.(&)` }, - { type: 'button' as const, content: 'Browse Research', href: '/projects/research', icon: 'mdi:arrow-right', style: 'accent' as const } - ], - inline: true, - display: 'flex' as const, - cardWidth - }, -]; diff --git a/src/lib/pages/research.ts b/src/lib/pages/research.ts deleted file mode 100644 index 090c11d..0000000 --- a/src/lib/pages/research.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { TerminalLine } from '$lib/components/tui/types'; -import { researchItems } from '$lib/config'; -import { projectNavLinks } from './projectNav'; -import { tagColor } from './palette'; - -const researchSections: TerminalLine[] = researchItems.flatMap(item => { - const infoChildren: TerminalLine[] = [ - { - type: 'output' as const, - content: `(&icon, mdi:microscope, 32) (&bg-cyan,black) RESEARCH (&)` - }, - ...(item.organization - ? [{ type: 'info' as const, content: `(&primary)${item.organization}(&)` }] - : []), - ...(item.period - ? [{ type: 'output' as const, content: `(&muted)${item.period}(&)` }] - : []), - ...(item.description - ? [{ type: 'output' as const, content: `(&text)${item.description}(&)` }] - : []), - ...(item.tech?.length - ? [{ type: 'output' as const, content: item.tech.map((tech, i) => `(&bg-${tagColor(i)},black) ${tech} (&)`).join(' ') }] - : []), - ...(item.link - ? [{ - type: 'group' as const, - content: '', - groupDirection: 'row' as const, - groupGap: '0.75rem', - groupExpand: true, - children: [ - { type: 'button' as const, content: 'View', href: item.link, icon: 'mdi:open-in-new', style: 'primary' as const, flex: true } - ] - }] - : []) - ]; - - return [ - { type: 'divider' as const, content: item.title.toUpperCase() }, - { type: 'group' as const, content: '', groupDirection: 'column' as const, groupAlign: 'stretch' as const, children: infoChildren }, - { type: 'blank' as const, content: '' } - ]; -}); - -export const lines: TerminalLine[] = [ - { type: 'command' as const, content: 'ls ~/projects/research' }, - projectNavLinks, - { type: 'blank' as const, content: '', height: '2rem' }, - { type: 'header' as const, content: 'Research' }, - { type: 'output' as const, content: `(&muted)Research roles, projects, and publications.(&)` }, - { type: 'blank' as const, content: '' }, - ...researchSections -]; diff --git a/src/lib/server/discord.ts b/src/lib/server/discord.ts new file mode 100644 index 0000000..5180e8c --- /dev/null +++ b/src/lib/server/discord.ts @@ -0,0 +1,71 @@ +import { ActivityType, Client, GatewayIntentBits, Partials, type Activity, type GuildMember } from 'discord.js'; +import { DISCORD_GUILD_ID, DISCORD_TOKEN, DISCORD_USER_ID } from '$app/env/private'; +import { offlinePresence, type DiscordActivity, type DiscordPresence, type DiscordStatus } from '#lib/discord/types'; + +const customStatusType = ActivityType.Custom; + +let client: Client | null = null; + +export function getDiscordClient(): Client | null { + if (!DISCORD_TOKEN) return null; + if (client) return client; + + client = new Client({ + partials: [Partials.User], + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildPresences] + }); + client.login(DISCORD_TOKEN).catch((error) => { + console.error('Discord login failed:', error); + client = null; + }); + return client; +} + +function assetUrl(asset: string | null | undefined, applicationId: string | null): string | null { + if (!asset) return null; + if (asset.startsWith('mp:external/')) return `https://media.discordapp.net/${asset.slice(3)}`; + if (/^https?:\/\//.test(asset)) return asset; + return applicationId ? `https://cdn.discordapp.com/app-assets/${applicationId}/${asset}.webp` : null; +} + +function toActivity(activity: Activity): DiscordActivity { + return { + name: activity.name, + type: activity.type, + details: activity.details ?? activity.state ?? null, + largeImage: assetUrl(activity.assets?.largeImage, activity.applicationId), + smallImage: assetUrl(activity.assets?.smallImage, activity.applicationId) + }; +} + +function toPresence(member: GuildMember): DiscordPresence { + const user = member.user; + const primaryGuild = user.primaryGuild; + const badgeGuildId = primaryGuild?.identityGuildId; + const badge = primaryGuild?.badge; + + return { + status: (member.presence?.status ?? 'offline') as DiscordStatus, + displayName: member.displayName, + avatarUrl: user.displayAvatarURL({ size: 128 }), + guildTag: primaryGuild?.tag ?? null, + guildTagBadgeUrl: + badgeGuildId && badge ? `https://cdn.discordapp.com/clan-badges/${badgeGuildId}/${badge}.png?size=32` : null, + activities: (member.presence?.activities ?? []) + .filter((activity) => activity.type !== customStatusType) + .map(toActivity) + }; +} + +export async function getDiscordPresence(): Promise { + const discordClient = getDiscordClient(); + if (!discordClient?.isReady() || !DISCORD_GUILD_ID || !DISCORD_USER_ID) return offlinePresence; + + try { + const guild = await discordClient.guilds.fetch(DISCORD_GUILD_ID); + const member = await guild.members.fetch(DISCORD_USER_ID); + return toPresence(member); + } catch { + return offlinePresence; + } +} diff --git a/src/lib/site/pages.ts b/src/lib/site/pages.ts new file mode 100644 index 0000000..6eb9986 --- /dev/null +++ b/src/lib/site/pages.ts @@ -0,0 +1,41 @@ +import { projectCategories } from '#lib/content'; + +export interface SitePage { + name: string; + path: string; + title: string; + description: string; + icon: string; + children?: SitePage[]; +} + +export const sitePages: SitePage[] = [ + { name: 'about', path: '/about', title: 'About', description: 'Background, hobbies, games, shows and movies.', icon: 'ph:user-circle-duotone' }, + { name: 'portfolio', path: '/portfolio', title: 'Portfolio', description: 'Profile, experience, skills and contact info.', icon: 'ph:briefcase-duotone' }, + { + name: 'projects', + path: '/projects', + title: 'Projects', + description: 'Open source, packages, hackathons and research.', + icon: 'ph:folder-open-duotone', + children: projectCategories.map((category) => ({ + name: category.id, + path: `/projects/${category.id}`, + title: category.title, + description: category.summary, + icon: category.icon + })) + }, + { name: 'blog', path: '/blog', title: 'Blog', description: 'Notes and write-ups from my projects.', icon: 'ph:notebook-duotone' }, + { name: 'models', path: '/models', title: '3D Models', description: 'A collection of 3D models with an interactive viewer.', icon: 'ph:cube-duotone' }, + { name: 'links', path: '/links', title: 'Links', description: 'All my links with scannable QR codes.', icon: 'ph:qr-code-duotone' } +]; + +export const allSitePages: SitePage[] = sitePages.flatMap((page) => [page, ...(page.children ?? [])]); + +export function findSitePage(nameOrPath: string): SitePage | undefined { + const cleaned = nameOrPath.toLowerCase().replace(/^~?\/+|\/+$/g, ''); + return allSitePages.find( + (page) => page.name === cleaned || page.path === `/${cleaned}` + ); +} diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts deleted file mode 100644 index 8960ab3..0000000 --- a/src/lib/stores/theme.ts +++ /dev/null @@ -1,170 +0,0 @@ -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'; -import wintryTheme from '$lib/assets/themes/wintry.theme.json'; -import roseTheme from '$lib/assets/themes/rose.theme.json'; -import cerberusTheme from '$lib/assets/themes/cerberus.theme.json'; - -export type ColorTheme = 'arch' | 'catppuccin' | 'wintry' | 'rose' | 'cerberus'; -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; - accent: string; - background: string; - backgroundLight: string; - text: string; - textMuted: string; - border: string; - terminal: string; - terminalPrompt: string; - terminalUser: string; - terminalPath: string; - colorMap: ThemeColorMap; -} - -// Load themes from JSON files -const themes: Record = { - arch: archTheme as ThemeJson, - catppuccin: catppuccinTheme as ThemeJson, - wintry: wintryTheme as ThemeJson, - rose: roseTheme as ThemeJson, - cerberus: cerberusTheme as ThemeJson -}; - -// Export themes for external access -export { themes }; - -// 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: defaults -> theme palette -> theme colors (so text, primary, etc. are correct hex values) - const mergedColorMap: ThemeColorMap = { - ...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, - 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: mergedColorMap - }; -} - -// 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'); - if (stored === 'dark' || stored === 'light') return stored; - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - } - return 'dark'; -} - -function getInitialTheme(): ColorTheme { - if (browser) { - const stored = localStorage.getItem('colorTheme'); - if (stored && stored in themeColorsMap) return stored as ColorTheme; - } - return 'arch'; -} - -export const mode = writable(getInitialMode()); -export const colorTheme = writable(getInitialTheme()); - -export const themeColors = derived([mode, colorTheme], ([$mode, $colorTheme]) => { - return themeColorsMap[$colorTheme][$mode]; -}); - -// Subscribe to persist changes -if (browser) { - mode.subscribe((value) => { - localStorage.setItem('mode', value); - document.documentElement.setAttribute('data-mode', value); - }); - - colorTheme.subscribe((value) => { - localStorage.setItem('colorTheme', value); - document.documentElement.setAttribute('data-theme', value); - }); -} - -export function toggleMode() { - mode.update((m) => (m === 'dark' ? 'light' : 'dark')); -} - -export function setColorTheme(theme: ColorTheme) { - colorTheme.set(theme); -} - -export function getThemeIcon(theme: ColorTheme): string { - switch (theme) { - case 'arch': return 'mdi:arch'; - case 'catppuccin': return 'solar:cat-bold'; - case 'wintry': return 'mdi:snowflake'; - case 'rose': return 'mdi:flower-tulip'; - case 'cerberus': return 'iconoir:wolf-solid'; - default: return 'mdi:palette'; - } -} - -// 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/lib/terminal/commands/index.ts b/src/lib/terminal/commands/index.ts new file mode 100644 index 0000000..3ed877a --- /dev/null +++ b/src/lib/terminal/commands/index.ts @@ -0,0 +1,63 @@ +import { profile } from '#lib/content'; +import { allSitePages } from '#lib/site/pages'; +import { blankLine, errorLine, line, span } from '../output'; +import { infoCommands } from './info'; +import { systemCommands } from './system'; +import type { Command, CommandContext } from './types'; + +export type { Command, CommandContext } from './types'; + +const helpCommand: Command = { + name: 'help', + aliases: ['h', '?'], + description: 'List all available commands', + run: (_args, context) => { + commands + .filter((command) => !command.hidden) + .forEach((command) => { + const aliases = command.aliases?.length ? ` (${command.aliases.join(', ')})` : ''; + context.print( + line(span(command.name.padEnd(12), 'accent', { bold: true }), command.description, span(aliases, 'muted')) + ); + }); + context.print(blankLine, line(span('Tip: try ', 'muted'), span('fastfetch', 'accent'), span(', ', 'muted'), span('cowsay hi', 'accent'), span(' or ', 'muted'), span('cd projects', 'accent')), blankLine); + } +}; + +const pageShortcuts: Command[] = allSitePages.map((page) => ({ + name: page.name, + description: `Open ${page.title}`, + hidden: true, + run: (_args, context) => context.openPage(page.path) +})); + +const socialShortcuts: Command[] = profile.socials.map((social) => ({ + name: social.name.toLowerCase(), + description: `Open ${social.name}`, + hidden: true, + run: (_args, context) => context.openUrl(social.url) +})); + +export const commands: Command[] = [helpCommand, ...infoCommands, ...systemCommands, ...pageShortcuts, ...socialShortcuts]; + +export function findCommand(name: string): Command | undefined { + const lowerName = name.toLowerCase(); + return commands.find((command) => command.name === lowerName || command.aliases?.includes(lowerName)); +} + +export function visibleCommandNames(): string[] { + return commands.filter((command) => !command.hidden).map((command) => command.name); +} + +export function runCommand(input: string, context: CommandContext) { + const trimmed = input.trim(); + if (!trimmed) return; + + const [name, ...args] = trimmed.split(/\s+/); + const command = findCommand(name); + if (!command) { + context.print(errorLine(`command not found: ${name}. Type help for a list`)); + return; + } + command.run(args, context); +} diff --git a/src/lib/terminal/commands/info.ts b/src/lib/terminal/commands/info.ts new file mode 100644 index 0000000..16bc798 --- /dev/null +++ b/src/lib/terminal/commands/info.ts @@ -0,0 +1,82 @@ +import { experience, profile, projectCategories, findProjectCategory } from '#lib/content'; +import { blankLine, errorLine, line, link, span, type Tone } from '../output'; +import type { Command } from './types'; +import { whoamiCommand } from './whoami'; + +const skillTones: Tone[] = ['blue', 'magenta', 'green', 'yellow', 'orange', 'cyan', 'accent']; + +export const infoCommands: Command[] = [ + whoamiCommand, + { + name: 'fastfetch', + aliases: ['neofetch', 'banner'], + description: 'Display system info', + run: (_args, context) => context.print({ kind: 'fetch' }) + }, + { + name: 'skills', + description: 'List skills by category', + run: (_args, context) => { + Object.entries(profile.skills).forEach(([category, skills], index) => { + const tone = skillTones[index % skillTones.length]; + context.print(line(span(category, tone, { bold: true })), line(' ', span(skills.join(' · '), 'foreground'))); + }); + context.print(blankLine); + } + }, + { + name: 'contact', + aliases: ['socials'], + description: 'Show social links', + run: (_args, context) => { + context.print(line(span('Find me online:', 'accent', { bold: true }))); + profile.socials.forEach((social) => + context.print(line(span(` ${social.name.padEnd(10)}`, 'muted'), link(social.url.replace(/^mailto:/, ''), social.url))) + ); + context.print(blankLine); + } + }, + { + name: 'email', + description: 'Show contact email', + run: (_args, context) => context.print(line(link(profile.email, `mailto:${profile.email}`))) + }, + { + name: 'experience', + aliases: ['work'], + description: 'Show work experience', + run: (_args, context) => { + experience.forEach((job) => { + context.print( + line(span(job.role, 'orange', { bold: true }), span(` @ ${job.organization}`, 'accent')), + line(span(` ${job.period}`, 'muted')) + ); + job.awards?.forEach((award) => context.print(line(span(` * ${award}`, 'green')))); + }); + context.print(blankLine); + } + }, + { + name: 'projects', + description: 'List projects, optionally by category', + usage: 'projects [opensource|packages|hackathons|research]', + argumentOptions: () => projectCategories.map((category) => category.id), + run: (args, context) => { + const categories = args[0] ? [findProjectCategory(args[0])] : projectCategories; + if (!categories[0]) { + context.print(errorLine(`unknown category ${args[0]}`)); + return; + } + categories.forEach((category) => { + if (!category) return; + context.print(line(span(`${category.title}/`, 'blue', { bold: true }))); + category.projects.forEach((project, index) => { + const branch = index === category.projects.length - 1 ? '└──' : '├──'; + const marker = project.awards?.length ? span(' [award]', 'yellow') : ''; + context.print(line(span(` ${branch} `, 'muted'), project.name, marker)); + }); + }); + context.print(line(span('Open one with ', 'muted'), span('cd projects/', 'accent')), blankLine); + } + } +]; diff --git a/src/lib/terminal/commands/system.ts b/src/lib/terminal/commands/system.ts new file mode 100644 index 0000000..7a11753 --- /dev/null +++ b/src/lib/terminal/commands/system.ts @@ -0,0 +1,134 @@ +import { profile } from '#lib/content'; +import { themes } from '#lib/theme/themes'; +import { allSitePages, findSitePage, sitePages } from '#lib/site/pages'; +import { blankLine, errorLine, line, span } from '../output'; +import type { Command } from './types'; + +function cowsay(message: string): string[] { + const text = message || 'Moo?'; + const border = '-'.repeat(text.length + 2); + return [ + ` ${border}`, + `< ${text} >`, + ` ${border}`, + ' \\ ^__^', + ' \\ (oo)\\_______', + ' (__)\\ )\\/\\', + ' ||----w |', + ' || ||' + ]; +} + +function easternDate(): string { + return new Date().toLocaleString('en-US', { + timeZone: 'America/New_York', + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short' + }); +} + +export const systemCommands: Command[] = [ + { + name: 'clear', + aliases: ['cls'], + description: 'Clear the terminal', + run: (_args, context) => context.clear() + }, + { + name: 'ls', + aliases: ['dir', 'tree'], + description: 'List the pages you can open', + run: (_args, context) => { + context.print(line(span('~', 'blue', { bold: true }))); + sitePages.forEach((page, pageIndex) => { + const isLastPage = pageIndex === sitePages.length - 1; + context.print(line(span(isLastPage ? '└── ' : '├── ', 'muted'), span(`${page.name}/`, 'blue', { bold: true }))); + page.children?.forEach((child, childIndex) => { + const isLastChild = childIndex === page.children!.length - 1; + const indent = isLastPage ? ' ' : '│ '; + context.print(line(span(`${indent}${isLastChild ? '└── ' : '├── '}`, 'muted'), span(`${child.name}/`, 'cyan'))); + }); + }); + context.print(blankLine); + } + }, + { + name: 'cd', + aliases: ['open', 'goto'], + description: 'Open a page in a window', + usage: 'cd , for example cd projects/hackathons', + argumentOptions: () => ['~', ...allSitePages.map((page) => page.path.slice(1))], + run: (args, context) => { + const target = args[0] ?? '~'; + if (target === '~' || target === '/' || target === 'home') { + context.openPage('/'); + return; + } + const page = findSitePage(target); + if (!page) { + context.print(errorLine(`no such page: ${target}. Try ls`)); + return; + } + context.print(line(span('opening ', 'muted'), span(page.path, 'accent'))); + context.openPage(page.path); + } + }, + { + name: 'theme', + description: 'List or set the color theme', + usage: 'theme [name]', + argumentOptions: () => themes.map((theme) => theme.id), + run: (args, context) => { + if (!args[0]) { + themes.forEach((theme) => { + const isCurrent = theme.id === context.currentThemeId(); + context.print(line(span(isCurrent ? '* ' : ' ', 'accent'), span(theme.id, isCurrent ? 'accent' : 'foreground'), span(` ${theme.mode}`, 'muted'))); + }); + return; + } + if (context.setTheme(args[0].toLowerCase())) context.print(line(span('theme set to ', 'muted'), span(args[0], 'accent'))); + else context.print(errorLine(`unknown theme ${args[0]}. Run theme to list them`)); + } + }, + { + name: 'echo', + description: 'Print text back', + usage: 'echo ', + run: (args, context) => context.print(line(args.join(' '))) + }, + { + name: 'cowsay', + description: 'Make a cow say something', + usage: 'cowsay ', + run: (args, context) => context.print(...cowsay(args.join(' ')).map((text) => line(span(text, 'accent')))) + }, + { + name: 'date', + description: 'Show the current date and time (Eastern)', + run: (_args, context) => context.print(line(easternDate())) + }, + { + name: 'exit', + description: 'Close this terminal', + run: (_args, context) => context.exit() + }, + { + name: 'sudo', + hidden: true, + description: 'Elevated privileges', + run: (_args, context) => + context.print(errorLine(`${profile.username} is not in the sudoers file. This incident will be reported.`)) + }, + { + name: 'pwd', + hidden: true, + description: 'Print working directory', + run: (_args, context) => context.print(line(`/home/${profile.username}`)) + } +]; diff --git a/src/lib/terminal/commands/types.ts b/src/lib/terminal/commands/types.ts new file mode 100644 index 0000000..0534ee8 --- /dev/null +++ b/src/lib/terminal/commands/types.ts @@ -0,0 +1,21 @@ +import type { OutputBlock } from '../output'; + +export interface CommandContext { + print: (...blocks: OutputBlock[]) => void; + clear: () => void; + exit: () => void; + openPage: (path: string) => void; + openUrl: (url: string) => void; + setTheme: (themeId: string) => boolean; + currentThemeId: () => string; +} + +export interface Command { + name: string; + description: string; + usage?: string; + aliases?: string[]; + hidden?: boolean; + argumentOptions?: () => string[]; + run: (args: string[], context: CommandContext) => void; +} diff --git a/src/lib/terminal/commands/whoami.ts b/src/lib/terminal/commands/whoami.ts new file mode 100644 index 0000000..09935f9 --- /dev/null +++ b/src/lib/terminal/commands/whoami.ts @@ -0,0 +1,52 @@ +import { experience, profile } from '#lib/content'; +import { blankLine, line, link, span, type OutputBlock, type Span } from '../output'; +import type { Command } from './types'; + +const labelWidth = 11; + +function field(label: string, ...value: (string | Span)[]): OutputBlock { + return line(span(` ${label.padEnd(labelWidth)}`, 'blue', { bold: true }), ...value); +} + +function joinWithDots(spans: Span[]): Span[] { + return spans.flatMap((item, index) => (index === 0 ? [item] : [span(' · ', 'muted'), item])); +} + +function whoamiOutput(): OutputBlock[] { + const currentRole = experience[0]; + const header = `${profile.username}@${profile.hostname}`; + const interests = profile.interests; + const socialLinks = profile.socials.map((social) => link(social.name.toLowerCase(), social.url)); + + return [ + line(span(profile.username, 'accent', { bold: true }), span('@', 'muted'), span(profile.hostname, 'accent', { bold: true })), + line(span('─'.repeat(header.length), 'muted')), + field('alias', profile.displayName), + field('role', profile.title), + field('school', `${profile.education.school}, ${profile.education.degree}`), + ...(currentRole ? [field('now', span(currentRole.role, 'orange'), span(' @ ', 'muted'), currentRole.organization)] : []), + ...(interests.length ? [field('interests', ...joinWithDots(interests.map((interest) => span(interest, 'magenta'))))] : []), + blankLine, + line(span(' ', 'muted'), span(profile.bio, 'foreground')), + blankLine, + field('links', ...joinWithDots(socialLinks)), + blankLine, + line( + span(' try ', 'muted'), + span('skills', 'green'), + span(', ', 'muted'), + span('experience', 'green'), + span(', ', 'muted'), + span('projects', 'green'), + span(' or ', 'muted'), + span('cd portfolio', 'green') + ), + blankLine + ]; +} + +export const whoamiCommand: Command = { + name: 'whoami', + description: 'Show who I am', + run: (_args, context) => context.print(...whoamiOutput()) +}; diff --git a/src/lib/terminal/completion.ts b/src/lib/terminal/completion.ts new file mode 100644 index 0000000..3c5a753 --- /dev/null +++ b/src/lib/terminal/completion.ts @@ -0,0 +1,37 @@ +import { findCommand, visibleCommandNames } from './commands'; + +export interface CompletionResult { + input: string; + options: string[]; +} + +function sharedPrefix(words: string[]): string { + return words.reduce((prefix, word) => { + let length = 0; + while (length < prefix.length && prefix[length] === word[length]) length++; + return prefix.slice(0, length); + }); +} + +function candidatesFor(words: string[]): string[] { + if (words.length <= 1) return visibleCommandNames(); + if (words.length > 2) return []; + return findCommand(words[0])?.argumentOptions?.() ?? []; +} + +export function complete(input: string): CompletionResult { + const words = input.trimStart().split(/\s+/); + const partial = words.at(-1) ?? ''; + const matches = [...new Set(candidatesFor(words))].filter((candidate) => + candidate.toLowerCase().startsWith(partial.toLowerCase()) + ); + + if (matches.length === 0) return { input, options: [] }; + + const before = input.slice(0, input.length - partial.length); + if (matches.length === 1) return { input: `${before}${matches[0]} `, options: [] }; + + const prefix = sharedPrefix(matches); + const madeProgress = prefix.length > partial.length; + return { input: `${before}${madeProgress ? prefix : partial}`, options: madeProgress ? [] : matches.sort() }; +} diff --git a/src/lib/terminal/output.ts b/src/lib/terminal/output.ts new file mode 100644 index 0000000..29fa6d9 --- /dev/null +++ b/src/lib/terminal/output.ts @@ -0,0 +1,41 @@ +export type Tone = + | 'foreground' + | 'muted' + | 'accent' + | 'red' + | 'green' + | 'yellow' + | 'orange' + | 'blue' + | 'cyan' + | 'magenta'; + +export interface Span { + text: string; + tone?: Tone; + bold?: boolean; + href?: string; +} + +export type OutputBlock = + | { kind: 'prompt'; command: string } + | { kind: 'line'; spans: Span[] } + | { kind: 'fetch' }; + +export function span(text: string, tone?: Tone, options: Omit = {}): Span { + return { text, tone, ...options }; +} + +export function line(...parts: (string | Span)[]): OutputBlock { + return { kind: 'line', spans: parts.map((part) => (typeof part === 'string' ? { text: part } : part)) }; +} + +export function link(text: string, href: string, tone: Tone = 'accent'): Span { + return { text, tone, href }; +} + +export function errorLine(message: string): OutputBlock { + return line(span('error: ', 'red', { bold: true }), message); +} + +export const blankLine: OutputBlock = line(''); diff --git a/src/lib/theme/stylesheet.ts b/src/lib/theme/stylesheet.ts new file mode 100644 index 0000000..4a956bd --- /dev/null +++ b/src/lib/theme/stylesheet.ts @@ -0,0 +1,30 @@ +import { themes, type Theme } from './themes'; + +function toCssName(key: string): string { + return `--${key.replaceAll('_', '-')}`; +} + +function themeVariables(theme: Theme): string { + const colorVariables = Object.entries(theme.colors).map( + ([key, value]) => `${toCssName(key)}: ${value};` + ); + const [borderStart, borderEnd] = theme.activeBorder ?? [theme.colors.accent, theme.colors.accent]; + const wallpaper = theme.wallpaper ? `url('${theme.wallpaper}')` : 'none'; + + return [ + ...colorVariables, + `--border-active: linear-gradient(45deg, ${borderStart}, ${borderEnd});`, + `--border-active-solid: ${borderStart};`, + `--wallpaper: ${wallpaper};`, + `color-scheme: ${theme.mode};` + ].join(' '); +} + +export function buildThemeStylesheet(defaultThemeId: string): string { + const defaultTheme = themes.find((theme) => theme.id === defaultThemeId) ?? themes[0]; + const defaultRule = `:root { ${themeVariables(defaultTheme)} }`; + const themeRules = themes.map( + (theme) => `:root[data-theme='${theme.id}'] { ${themeVariables(theme)} }` + ); + return [defaultRule, ...themeRules].join('\n'); +} diff --git a/src/lib/theme/theme.svelte.ts b/src/lib/theme/theme.svelte.ts new file mode 100644 index 0000000..2dfdf5c --- /dev/null +++ b/src/lib/theme/theme.svelte.ts @@ -0,0 +1,48 @@ +import { desktop } from '#lib/content'; +import { findTheme, themes, type Theme } from './themes'; + +export const themeStorageKey = 'theme'; + +function readStoredThemeId(): string | null { + try { + return localStorage.getItem(themeStorageKey); + } catch { + return null; + } +} + +function storeThemeId(themeId: string) { + try { + localStorage.setItem(themeStorageKey, themeId); + } catch {} +} + +class ThemeState { + currentId = $state(desktop.defaultTheme); + + get current(): Theme { + return findTheme(this.currentId) ?? themes[0]; + } + + restore() { + const storedTheme = findTheme(readStoredThemeId()); + if (storedTheme) this.currentId = storedTheme.id; + } + + set(themeId: string): boolean { + const theme = findTheme(themeId); + if (!theme) return false; + this.currentId = theme.id; + document.documentElement.dataset.theme = theme.id; + storeThemeId(theme.id); + return true; + } + + cycle(step: number) { + const index = themes.findIndex((theme) => theme.id === this.currentId); + const nextIndex = (index + step + themes.length) % themes.length; + this.set(themes[nextIndex].id); + } +} + +export const themeState = new ThemeState(); diff --git a/src/lib/theme/themes.ts b/src/lib/theme/themes.ts new file mode 100644 index 0000000..256f976 --- /dev/null +++ b/src/lib/theme/themes.ts @@ -0,0 +1,321 @@ +export interface ThemePalette { + accent: string; + selection: string; + muted: string; + background: string; + dark_background: string; + lighter_background: string; + foreground: string; + red: string; + yellow: string; + orange: string; + green: string; + cyan: string; + blue: string; + magenta: string; +} + +export interface Theme { + id: string; + name: string; + mode: 'dark' | 'light'; + wallpaper?: string; + activeBorder?: [string, string]; + colors: ThemePalette; +} + +export const themes: Theme[] = [ + { + id: 'flats', + name: 'Flats', + mode: 'dark', + wallpaper: '/wallpapers/pastel-car.jpg', + colors: { + accent: '#5fa3e8', + selection: '#1e2228', + muted: '#78828c', + background: '#0f1012', + dark_background: '#0a0b0c', + lighter_background: '#191c20', + foreground: '#cbd2d9', + red: '#d53838', + yellow: '#d58638', + orange: '#d56338', + green: '#38d586', + cyan: '#38d4d5', + blue: '#3886d5', + magenta: '#d53886' + } + }, + { + id: 'retro-82', + name: 'Retro 82', + mode: 'dark', + wallpaper: '/wallpapers/pixel-city.png', + activeBorder: ['#faa968', '#f85525'], + colors: { + accent: '#faa968', + selection: '#134e5a', + muted: '#3f8f8a', + background: '#05182e', + dark_background: '#031222', + lighter_background: '#0a2540', + foreground: '#f6dcac', + red: '#f85525', + yellow: '#e97b3c', + orange: '#faa968', + green: '#028391', + cyan: '#8cbfb8', + blue: '#3f8f8a', + magenta: '#a7c9c6' + } + }, + { + id: 'tokyo-night', + name: 'Tokyo Night', + mode: 'dark', + wallpaper: '/wallpapers/home127-dark.jpg', + colors: { + accent: '#7aa2f7', + selection: '#292e42', + muted: '#565f89', + background: '#1a1b26', + dark_background: '#13141c', + lighter_background: '#24283b', + foreground: '#a9b1d6', + red: '#f7768e', + yellow: '#e0af68', + orange: '#eb927b', + green: '#9ece6a', + cyan: '#449dab', + blue: '#7aa2f7', + magenta: '#ad8ee6' + } + }, + { + id: 'catppuccin', + name: 'Catppuccin', + mode: 'dark', + wallpaper: '/wallpapers/cat.jpg', + colors: { + accent: '#89b4fa', + selection: '#45475a', + muted: '#6c7086', + background: '#1e1e2e', + dark_background: '#161622', + lighter_background: '#313244', + foreground: '#cdd6f4', + red: '#f38ba8', + yellow: '#f9e2af', + orange: '#fab387', + green: '#a6e3a1', + cyan: '#94e2d5', + blue: '#89b4fa', + magenta: '#cba6f7' + } + }, + { + id: 'arch', + name: 'Arch Linux', + mode: 'dark', + wallpaper: '/wallpapers/pacman-black.png', + activeBorder: ['#33ccff', '#00ff99'], + colors: { + accent: '#1793d1', + selection: '#21262d', + muted: '#8b949e', + background: '#0d1117', + dark_background: '#090c10', + lighter_background: '#161b22', + foreground: '#c9d1d9', + red: '#ff7b72', + yellow: '#d29922', + orange: '#ffa657', + green: '#3fb950', + cyan: '#39c5cf', + blue: '#58a6ff', + magenta: '#bc8cff' + } + }, + { + id: 'gruvbox', + name: 'Gruvbox', + mode: 'dark', + wallpaper: '/wallpapers/deer-forest.jpg', + colors: { + accent: '#7daea3', + selection: '#504945', + muted: '#928374', + background: '#282828', + dark_background: '#1e1e1e', + lighter_background: '#3c3836', + foreground: '#d4be98', + red: '#ea6962', + yellow: '#d8a657', + orange: '#e1875c', + green: '#a9b665', + cyan: '#89b482', + blue: '#7daea3', + magenta: '#d3869b' + } + }, + { + id: 'nord', + name: 'Nord', + mode: 'dark', + wallpaper: '/wallpapers/nord_minimal_cat.png', + colors: { + accent: '#81a1c1', + selection: '#434c5e', + muted: '#7b88a1', + background: '#2e3440', + dark_background: '#222730', + lighter_background: '#3b4252', + foreground: '#d8dee9', + red: '#bf616a', + yellow: '#ebcb8b', + orange: '#d5967a', + green: '#a3be8c', + cyan: '#88c0d0', + blue: '#81a1c1', + magenta: '#b48ead' + } + }, + { + id: 'kanagawa', + name: 'Kanagawa', + mode: 'dark', + wallpaper: '/wallpapers/pixel-earth.png', + colors: { + accent: '#dcd7ba', + selection: '#363646', + muted: '#727169', + background: '#1f1f28', + dark_background: '#17171e', + lighter_background: '#2a2a37', + foreground: '#dcd7ba', + red: '#c34043', + yellow: '#c0a36e', + orange: '#c17158', + green: '#76946a', + cyan: '#6a9589', + blue: '#7e9cd8', + magenta: '#957fb8' + } + }, + { + id: 'wintry', + name: 'Wintry', + mode: 'dark', + wallpaper: '/wallpapers/pixel-earth.png', + colors: { + accent: '#7dd3fc', + selection: '#334155', + muted: '#94a3b8', + background: '#0f172a', + dark_background: '#0a1020', + lighter_background: '#1e293b', + foreground: '#f1f5f9', + red: '#f43f5e', + yellow: '#fde047', + orange: '#fb923c', + green: '#4ade80', + cyan: '#67e8f9', + blue: '#60a5fa', + magenta: '#c084fc' + } + }, + { + id: 'rose', + name: 'Rose', + mode: 'dark', + wallpaper: '/wallpapers/cat.jpg', + colors: { + accent: '#fb7185', + selection: '#5f484f', + muted: '#c48b95', + background: '#1f181b', + dark_background: '#171214', + lighter_background: '#2e2327', + foreground: '#ffe4e6', + red: '#f43f5e', + yellow: '#fde047', + orange: '#fb923c', + green: '#34d399', + cyan: '#67e8f9', + blue: '#60a5fa', + magenta: '#e879f9' + } + }, + { + id: 'cerberus', + name: 'Cerberus', + mode: 'dark', + wallpaper: '/wallpapers/home127-dark.jpg', + activeBorder: ['#818cf8', '#e879f9'], + colors: { + accent: '#818cf8', + selection: '#404040', + muted: '#a3a3a3', + background: '#171717', + dark_background: '#0f0f0f', + lighter_background: '#262626', + foreground: '#f5f5f5', + red: '#ef4444', + yellow: '#fbbf24', + orange: '#fb923c', + green: '#2dd4bf', + cyan: '#67e8f9', + blue: '#818cf8', + magenta: '#e879f9' + } + }, + { + id: 'catppuccin-latte', + name: 'Catppuccin Latte', + mode: 'light', + wallpaper: '/wallpapers/yosemite-color-block.jpg', + colors: { + accent: '#1e66f5', + selection: '#ccd0da', + muted: '#8c8fa1', + background: '#eff1f5', + dark_background: '#e3e4e8', + lighter_background: '#dce0e8', + foreground: '#4c4f69', + red: '#d20f39', + yellow: '#df8e1d', + orange: '#d84e2b', + green: '#40a02b', + cyan: '#179299', + blue: '#1e66f5', + magenta: '#ea76cb' + } + }, + { + id: 'rose-pine', + name: 'Rose Pine Dawn', + mode: 'light', + wallpaper: '/wallpapers/material-pastel.jpg', + colors: { + accent: '#56949f', + selection: '#dfdad9', + muted: '#9893a5', + background: '#faf4ed', + dark_background: '#ede7e1', + lighter_background: '#f2e9e1', + foreground: '#575279', + red: '#b4637a', + yellow: '#ea9d34', + orange: '#cf8057', + green: '#286983', + cyan: '#d7827e', + blue: '#56949f', + magenta: '#907aa9' + } + } +]; + +export function findTheme(id: string | null | undefined): Theme | undefined { + return themes.find((theme) => theme.id === id); +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 90543af..c901c02 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,152 +1,89 @@ - - {status} | {user.username} - - +
+ -
- +
= 500}>{asciiDigits.join('\n')}
+ +

x Error {status}: {error.message}

+

# {funMessage}

+ +
+
$ cat suggestions.txt
+
> Check if the URL is correct
+
> Try refreshing the page
+
> Contact me if the problem persists
+
+ +
+ Go Home + Projects +
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts deleted file mode 100644 index add3c31..0000000 --- a/src/routes/+layout.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { LayoutServerLoad } from './$types'; - -import { bot } from '$lib/discordbot'; - -export const load: LayoutServerLoad = async () => { - const info = await bot.getUserActivityInGuild().catch(() => null); - - return { - status: info?.status ?? 'offline', - activities: info?.activities ?? [], - displayName: info?.displayName ?? null, - avatarUrl: info?.avatarUrl ?? null, - guildTag: info?.guildTag ?? null, - guildTagBadgeImage: info?.guildTagBadgeImage ?? null - }; -}; \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b98a28e..87c0d90 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,126 +1,63 @@ + {@html themeStyleTag} - Blob's Portfolio - + -{#if mounted} -
- - -
- {@render children()} -
-
-{:else} - -
-
- Initializing terminal... - -
-
-{/if} + handleKeybind(event, openWindowIds)} /> + +
+ + + {@render children()} + + {#if windowManager.launcherOpen} + + {/if} +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0bfab2c..6486c74 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,26 +1,8 @@ - {site.title} - + {profile.displayName} | Portfolio + - -
- -
- - diff --git a/src/routes/about/+page.server.ts b/src/routes/about/+page.server.ts deleted file mode 100644 index f10fd93..0000000 --- a/src/routes/about/+page.server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import playdl, { type YouTubeVideo } from 'play-dl'; - -function shuffleArray(array: any[]) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; -} - -/** @type {import('./$types').PageServerLoad} */ -export async function load({ params }) { - - // const playlistUrl = 'https://music.youtube.com/playlist?list=PLHqPHHvmOjJ6JwiYc3Fg-O5JGbVppI1VR&si=evK225CpqXKeSq2Q'; - - // const playlist = await playdl.playlist_info(playlistUrl); - - // let songs = (await playlist.all_videos()).map((video: YouTubeVideo) => { - // return { - // title: video.title, - // url: video.url, - // author: video.channel?.name, - // authorUrl: video.channel?.url, - // thumbnail: video.thumbnails[0].url, - // duration: video.durationRaw, - // } - // }) - - return { - songs: [] - }; - -} \ No newline at end of file diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index e6b8062..23f96ae 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,215 +1,83 @@ - - About | {user.displayname} - - +
+ -
- + + + +
+
+ {#each about.hobbies as hobby (hobby.name)} + +

{hobby.description}

+
+ {/each} +
+
+ +
+
+ {#each about.games as game (game.name)} + + {game.name} + Play + + {/each} +
+
+ + {#each [{ id: 'shows', title: 'TV Shows', items: shows }, { id: 'movies', title: 'Movies', items: movies }] as group (group.id)} +
+
+ {#each group.items as item (item.name)} + +
+ + {item.rating}/10 +
+
+ {/each} +
+
+ {/each}
diff --git a/src/routes/api/discord/+server.ts b/src/routes/api/discord/+server.ts new file mode 100644 index 0000000..dd315f5 --- /dev/null +++ b/src/routes/api/discord/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import { getDiscordPresence } from '#lib/server/discord'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async () => { + const presence = await getDiscordPresence(); + return json(presence, { headers: { 'Cache-Control': 'public, max-age=15' } }); +}; diff --git a/src/routes/blog/+page.server.ts b/src/routes/blog/+page.server.ts index 39a476b..6928df2 100644 --- a/src/routes/blog/+page.server.ts +++ b/src/routes/blog/+page.server.ts @@ -1,19 +1,7 @@ +import { getAllTags, getPostSummaries } from '#lib/blog/posts'; import type { PageServerLoad } from './$types'; -import { getAllPosts, getAllTags } from '$lib/blog/posts'; -export const load: PageServerLoad = () => { - const posts = getAllPosts().map(({ slug, title, author, date, tags, excerpt, readingTime }) => ({ - slug, - title, - author, - date, - tags, - excerpt, - readingTime - })); - - return { - posts, - tags: getAllTags() - }; -}; +export const load: PageServerLoad = () => ({ + posts: getPostSummaries(), + tags: getAllTags() +}); diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index 3ce593b..749192c 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -1,21 +1,15 @@ - - {meta?.title ?? `${user.displayname} — Blog`} - - +
+ + RSS Feed + -
-
-
+ (currentPage = 1)} /> -
- - -
-
-
-

{blogName}

-

{blogTagline}

- -
- - - - {#if data.tags.length} -
- - {#each data.tags as tag (tag)} - - {/each} -
- {/if} - - {#if filteredPosts.length === 0} -

- {data.posts.length === 0 ? 'No posts yet. Check back soon.' : 'No posts match your search.'} -

- {:else} -
- {#each paginatedPosts as post (post.slug)} - - -

{post.title}

-

{post.excerpt}

- {#if post.tags.length} - - {/if} -
- {/each} -
- - {#if totalPages > 1} - - {/if} - {/if} -
-
- - {}} /> + {#if data.tags.length} +
+ + {#each data.tags as tag (tag)} + + {/each}
-
+ {/if} + + {#if filteredPosts.length === 0} +

{data.posts.length === 0 ? 'No posts yet. Check back soon.' : 'No posts match your search.'}

+ {:else} +
+ {#each visiblePosts as post (post.slug)} + +
+ {post.author} + {formatPostDate(post.date)} + {post.readingTime} min read +
+

{post.title}

+

{post.excerpt}

+ +
+ {/each} +
+ + {#if totalPages > 1} + + {/if} + {/if}
diff --git a/src/routes/blog/[slug]/+page.server.ts b/src/routes/blog/[slug]/+page.server.ts index 87b6774..e7e6e62 100644 --- a/src/routes/blog/[slug]/+page.server.ts +++ b/src/routes/blog/[slug]/+page.server.ts @@ -1,15 +1,10 @@ import { error } from '@sveltejs/kit'; +import { getAdjacentPosts, getPostBySlug } from '#lib/blog/posts'; import type { PageServerLoad } from './$types'; -import { getPostBySlug, getAdjacentPosts } from '$lib/blog/posts'; export const load: PageServerLoad = ({ params }) => { const post = getPostBySlug(params.slug); + if (!post) error(404, 'Post not found'); - if (!post) { - error(404, 'Post not found'); - } - - const { previous, next } = getAdjacentPosts(params.slug); - - return { post, previous, next }; + return { post, ...getAdjacentPosts(params.slug) }; }; diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte index ac2cefc..355f519 100644 --- a/src/routes/blog/[slug]/+page.svelte +++ b/src/routes/blog/[slug]/+page.svelte @@ -1,344 +1,96 @@ - - {data.post.title} | {user.displayname} - - +
+
+ -
-
-
- -
- - -
-
- ← back to blog - -

{data.post.title}

- - - - {#if data.post.tags.length} - - {/if} - -
- {@html data.post.html} -
- - {#if data.previous || data.next} - - {/if} -
-
- - {}} /> +
+ < back to blog + {data.post.author} + {formatPostDate(data.post.date)} + {data.post.readingTime} min read + {#each data.post.tags as tag (tag)} + #{tag} + {/each}
-
+ + + + {#if data.previous || data.next} + + {/if} +
diff --git a/src/routes/blog/rss.xml/+server.ts b/src/routes/blog/rss.xml/+server.ts index 0a80a7b..bf64c2a 100644 --- a/src/routes/blog/rss.xml/+server.ts +++ b/src/routes/blog/rss.xml/+server.ts @@ -1,7 +1,7 @@ +import { getAllPosts } from '#lib/blog/posts'; +import { formatRssDate } from '#lib/blog/date'; +import { blogSettings } from '#lib/content'; import type { RequestHandler } from './$types'; -import { getAllPosts } from '$lib/blog/posts'; -import { blogName, blogTagline } from '$lib/config/blog'; -import { formatRssDate } from '$lib/blog/date'; function escapeXml(text: string): string { return text @@ -15,14 +15,13 @@ function escapeXml(text: string): string { export const GET: RequestHandler = ({ url }) => { const items = getAllPosts() .map((post) => { - const link = `${url.origin}/blog/${post.slug}`; - const pubDate = formatRssDate(post.date); + const postUrl = `${url.origin}/blog/${post.slug}`; return ` ${escapeXml(post.title)} - ${link} - ${link} - ${pubDate} + ${postUrl} + ${postUrl} + ${formatRssDate(post.date)} ${escapeXml(post.author)} ${escapeXml(post.excerpt)} `; @@ -32,13 +31,11 @@ export const GET: RequestHandler = ({ url }) => { const xml = ` - ${escapeXml(blogName)} + ${escapeXml(blogSettings.name)} ${url.origin}/blog - ${escapeXml(blogTagline)}${items} + ${escapeXml(blogSettings.tagline)}${items} `; - return new Response(xml, { - headers: { 'Content-Type': 'application/xml' } - }); + return new Response(xml, { headers: { 'Content-Type': 'application/xml' } }); }; diff --git a/src/routes/components/+page.svelte b/src/routes/components/+page.svelte deleted file mode 100644 index e5f34a6..0000000 --- a/src/routes/components/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - Components | {user.displayname} - - - -
- -
- - diff --git a/src/routes/layout.css b/src/routes/layout.css index 8159040..627cbbe 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -1,13 +1,7 @@ -/* Font imports - must come first */ -/* @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); */ - @import 'tailwindcss'; - @plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; -/* Load local JetBrains Mono SemiBold from static folder so the site uses the exact semi-bold file */ @font-face { font-family: 'JetBrains Mono'; src: url('/font/JetBrainsMono-SemiBold.ttf') format('truetype'); @@ -16,259 +10,87 @@ font-display: swap; } -/* CSS Reset and Base Styles */ :root { - --font-mono: 'JetBrains Mono', monospace; - - /* Animation durations */ - --transition-fast: 150ms; - --transition-normal: 250ms; - --transition-slow: 400ms; - - /* Spacing */ - --navbar-height: 60px; -} - -/* Prefer the local semi-bold version when bold/600 weight is requested */ -code, pre, kbd, samp, .terminal, .mono, .prompt, .prompt-mini, .hero-title { - font-family: 'JetBrains Mono', var(--font-mono); - font-weight: 600; /* Use the semi-bold file */ -} - -/* Ensure headings using monospace also render the semibold weight */ -.heading-responsive, .hero-title, .terminal-title, .nav-brand { - font-family: 'JetBrains Mono', var(--font-mono); - font-weight: 600; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --bar-height: 34px; + --gaps-in: 4px; + --gaps-out: 12px; + --border-size: 3px; + --border-inactive: rgba(89, 89, 89, 0.67); } * { box-sizing: border-box; } -html { - scroll-behavior: smooth; - background: #1e1e2e; /* Fallback dark bg to prevent white flash on mobile */ -} - +html, body { margin: 0; - padding: 0; - font-family: var(--font-mono) !important; - -webkit-font-smoothing: antialiased; - -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 */ -code, pre, kbd, samp, .terminal, .mono { + height: 100%; + overflow: hidden; + background: var(--dark-background); + color: var(--foreground); font-family: var(--font-mono); + font-weight: 600; + font-size: 16px; + -webkit-font-smoothing: antialiased; } -/* Main content wrapper */ -.main-content { - padding-top: var(--navbar-height); - min-height: 100vh; - position: relative; - z-index: 1; -} - -/* Scrollbar styling - hidden but still scrollable */ -::-webkit-scrollbar { - display: none; -} - -/* For Firefox */ -html { - scrollbar-width: none; -} - -/* Keep scroll functionality */ -body { - -ms-overflow-style: none; /* IE and Edge */ -} - -/* Selection styling */ -::selection { - background: rgba(23, 147, 209, 0.3); - color: inherit; -} - -/* Focus styles */ -:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -/* Links */ a { color: inherit; text-decoration: none; - transition: color var(--transition-fast) ease; } -/* Buttons reset */ button { - font-family: inherit; + font: inherit; + color: inherit; cursor: pointer; } -/* Animation utilities */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } +::selection { + background: var(--selection); + color: var(--foreground); } -@keyframes fadeInUp { +:focus-visible { + outline: 1px solid var(--accent); + outline-offset: 2px; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--muted) transparent; +} + +@keyframes window-pop-in { from { opacity: 0; - transform: translateY(20px); + transform: scale(0.87); } to { opacity: 1; - transform: translateY(0); + transform: scale(1); } } -@keyframes fadeInDown { - from { +@keyframes cursor-blink { + 50% { opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); } } -@keyframes slideInLeft { - from { - opacity: 0; - transform: translateX(-30px); - } - to { - opacity: 1; - transform: translateX(0); - } +.page { + container-type: inline-size; + padding: 0.75rem 1.25rem 2rem; + line-height: 1.6; } -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } -} - -@keyframes glow { - 0%, 100% { - box-shadow: 0 0 5px currentColor, 0 0 10px currentColor; - } - 50% { - box-shadow: 0 0 10px currentColor, 0 0 20px currentColor, 0 0 30px currentColor; - } -} - -@keyframes typing { - from { width: 0; } - to { width: 100%; } -} - -@keyframes blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -/* Utility classes */ -.animate-fade-in { - animation: fadeIn 0.5s ease-out; -} - -.animate-fade-in-up { - animation: fadeInUp 0.5s ease-out; -} - -.animate-fade-in-down { - animation: fadeInDown 0.5s ease-out; -} - -.animate-slide-in-left { - animation: slideInLeft 0.5s ease-out; -} - -.animate-slide-in-right { - animation: slideInRight 0.5s ease-out; -} - -.animate-pulse { - animation: pulse 2s ease-in-out infinite; -} - -.animate-float { - animation: float 3s ease-in-out infinite; -} - -.animate-glow { - animation: glow 2s ease-in-out infinite; -} - -/* Stagger animation delays */ -.stagger-1 { animation-delay: 0.1s; } -.stagger-2 { animation-delay: 0.2s; } -.stagger-3 { animation-delay: 0.3s; } -.stagger-4 { animation-delay: 0.4s; } -.stagger-5 { animation-delay: 0.5s; } - -/* Glass effect */ -.glass { - background: rgba(255, 255, 255, 0.05); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -/* Terminal window styling */ -.terminal-window { - border-radius: 12px; - overflow: hidden; - box-shadow: - 0 25px 50px -12px rgba(0, 0, 0, 0.25), - 0 0 0 1px rgba(255, 255, 255, 0.05) inset; -} - -/* Container */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 1.5rem; -} - -/* Grid system */ -.grid-terminal { +.card-grid { display: grid; - gap: 1.5rem; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 0.75rem; } -/* Responsive text */ -.text-responsive { - font-size: clamp(1rem, 2.5vw, 1.25rem); -} - -.heading-responsive { - font-size: clamp(1.5rem, 5vw, 3rem); +.muted { + color: var(--muted); } diff --git a/src/routes/links/+page.svelte b/src/routes/links/+page.svelte index b0eb4d4..5e791ca 100644 --- a/src/routes/links/+page.svelte +++ b/src/routes/links/+page.svelte @@ -1,223 +1,134 @@ - - {meta?.title ?? `${user.displayname} — Links`} - - +
+ -