Rewrite website as a Hyprland desktop
This commit is contained in:
1 parent
7c52779cf6
commit
3065ad6092
197 files changed
+5609
-17779
No files matched your search
@@ -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 <page>` | `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 <text>` | | Print text back |
|
||||
| `cowsay <text>` | | 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 <page>`, `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
|
||||
<TerminalTUI
|
||||
lines={lines}
|
||||
title="~/directory"
|
||||
interactive={true}
|
||||
speed="normal"
|
||||
autoscroll={true}
|
||||
enableCommands={true}
|
||||
onComplete={() => console.log('Done!')}
|
||||
/>
|
||||
```
|
||||
|
||||
Props:
|
||||
- `lines` - Array of TerminalLine objects
|
||||
- `title` - Terminal window title
|
||||
- `interactive` - Enable keyboard navigation
|
||||
- `speed` - Typing speed preset or multiplier
|
||||
- `autoscroll` - Auto-scroll as content types (default: true)
|
||||
- `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
|
||||
<script lang="ts">
|
||||
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
|
||||
import type { TerminalAPI, TerminalLine } from '$lib';
|
||||
|
||||
let terminal = $state<TerminalAPI>();
|
||||
let lines = $state<TerminalLine[]>([
|
||||
{ type: 'output', content: 'Hello world!' }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<TerminalTUI bind:terminal bind:lines title="~/demo" />
|
||||
```
|
||||
|
||||
### 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
|
||||
<script lang="ts">
|
||||
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
|
||||
import type { TerminalAPI } from '$lib';
|
||||
|
||||
let terminal = $state<TerminalAPI>();
|
||||
let lines = $state([
|
||||
{ type: 'output', content: 'Initializing...', id: 'status' }
|
||||
]);
|
||||
|
||||
async function runProcess() {
|
||||
terminal?.updateById('status', {
|
||||
type: 'info',
|
||||
content: 'Processing...'
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
|
||||
terminal?.write({
|
||||
type: 'progress',
|
||||
content: '',
|
||||
progress: 50,
|
||||
progressLabel: '50%'
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
|
||||
terminal?.updateById('status', {
|
||||
type: 'success',
|
||||
content: 'Complete!'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<TerminalTUI bind:terminal bind:lines />
|
||||
<button onclick={runProcess}>Start</button>
|
||||
```
|
||||
|
||||
## 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
|
||||
<ModelViewer
|
||||
modelPath="/models/my-model.glb"
|
||||
modelName="My Model"
|
||||
/>
|
||||
```
|
||||
|
||||
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<ColorTheme, ThemeJson> = {
|
||||
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`.
|
||||
Reference in new issue
Block a user