Add mobile styles, social icons and README badges
This commit is contained in:
1 parent
be028d6934
commit
a0fd2fa2a1
23 files changed
+225
-174
No files matched your search
@@ -1,152 +1,65 @@
|
|||||||
# Sir Blob's Desktop
|
<div align="center">
|
||||||
|
<h1>Sir Blob's Desktop</h1>
|
||||||
|
<p>My portfolio website, built as a web version of my Arch Linux desktop.</p>
|
||||||
|
|
||||||
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.
|
<img src="https://img.shields.io/badge/Arch_Linux-1793D1?style=for-the-badge&logo=arch-linux&logoColor=white" alt="Arch Linux" />
|
||||||
|
<img src="https://img.shields.io/badge/Hyprland-00A86B?style=for-the-badge&logo=hyprland&logoColor=white" alt="Hyprland" />
|
||||||
|
<img src="https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=svelte&logoColor=white" alt="SvelteKit" />
|
||||||
|
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
||||||
|
<img src="https://img.shields.io/badge/Tailwind_CSS-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white" alt="Tailwind CSS" />
|
||||||
|
<img src="https://img.shields.io/badge/Three.js-000000?style=for-the-badge&logo=threedotjs&logoColor=white" alt="Three.js" />
|
||||||
|
<img src="https://img.shields.io/badge/Bun-000000?style=for-the-badge&logo=bun&logoColor=white" alt="Bun" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The site recreates my [blomarchy](https://git.sirblob.co/SirBlob/blomarchy) setup in the browser: a Hyprland-style tiling desktop, the Quickshell bar, and kitty terminals you can type into.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Tiling windows** - every page opens as a window that tiles like Hyprland's dwindle layout, with gradient borders.
|
||||||
|
- **Quickshell bar** - app launcher, workspaces, clock, live Discord status, battery and a theme picker.
|
||||||
|
- **Kitty terminals** - a real command line with history and tab completion. Try `fastfetch`, `whoami` or `cowsay`.
|
||||||
|
- **Desktop icons** - grouped into pages, terminal scripts and socials (GitHub, LinkedIn, Devpost, Discord).
|
||||||
|
- **13 themes** - Flats, Retro 82, Tokyo Night, Catppuccin, Arch Linux, Gruvbox, Nord, Kanagawa, Wintry, Rose, Cerberus, Catppuccin Latte and Rose Pine Dawn, each with its own wallpaper.
|
||||||
|
- **Portfolio** - about, portfolio, projects (open source, packages, hackathons, research), blog with RSS, 3D models and links with QR codes.
|
||||||
|
- **Mobile friendly** - on small screens windows go fullscreen with tabs to switch between them.
|
||||||
|
|
||||||
|
## Keybinds
|
||||||
|
|
||||||
|
| Keys | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| Alt + Enter | Open a terminal |
|
||||||
|
| Alt + Space | App launcher |
|
||||||
|
| Alt + Q | Close window |
|
||||||
|
| Alt + F | Fullscreen page |
|
||||||
|
| Alt + J / K | Next / previous window |
|
||||||
|
| Alt + 1-9 | Switch workspace |
|
||||||
|
| Alt + T | Next theme |
|
||||||
|
|
||||||
|
Alt stands in for Super, since browsers can't use it.
|
||||||
|
|
||||||
|
## Terminal commands
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `help` | list every command |
|
||||||
|
| `whoami` | who I am |
|
||||||
|
| `fastfetch` | system info |
|
||||||
|
| `skills` | skills by category |
|
||||||
|
| `experience` | work experience |
|
||||||
|
| `projects [category]` | list my projects |
|
||||||
|
| `contact` | where to find me |
|
||||||
|
| `ls` / `cd <page>` | browse and open pages |
|
||||||
|
| `theme [name]` | list or switch themes |
|
||||||
|
| `cowsay <text>` | moo |
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
`start` builds the site with Bun (so it does not depend on the system Node version) and serves it with Bun on `PORT` (default 3000), reading `.env` for the Discord variables.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
bun run dev
|
bun run dev
|
||||||
bun run build
|
|
||||||
bun run start
|
bun run start
|
||||||
bun run check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## How the desktop works
|
Site content lives in `content/`.
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Keybinds
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
| --- | --- |
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
### Projects
|
|
||||||
|
|
||||||
Every project in every category uses the same shape. Only `name`, `description`, `tags` and `links` are required.
|
|
||||||
|
|
||||||
```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://..." }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Featured projects are listed first.
|
|
||||||
|
|
||||||
### Desktop icons
|
|
||||||
|
|
||||||
Each icon in `content/desktop.json` has a `label`, an Iconify `icon` and one `open` action:
|
|
||||||
|
|
||||||
```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" } }
|
|
||||||
```
|
|
||||||
|
|
||||||
`{ "terminal": "" }` opens an empty terminal.
|
|
||||||
|
|
||||||
### Blog posts
|
|
||||||
|
|
||||||
Add a markdown file to `content/blog/`. The file name is the URL slug.
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
title: My Post Title
|
|
||||||
author: Sir Blob
|
|
||||||
date: 2026-01-01
|
|
||||||
tags: meta, notes
|
|
||||||
excerpt: One line summary for the list and the RSS feed.
|
|
||||||
---
|
|
||||||
|
|
||||||
Post content in markdown.
|
|
||||||
```
|
|
||||||
|
|
||||||
The RSS feed is at `/blog/rss.xml`.
|
|
||||||
|
|
||||||
## Themes
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
Shipped themes: Flats (default), Retro 82, Tokyo Night, Catppuccin, Arch Linux, Gruvbox, Nord, Kanagawa, Wintry, Rose, Cerberus, Catppuccin Latte, Rose Pine Dawn.
|
|
||||||
|
|
||||||
## Terminal commands
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
Commands live in `src/lib/terminal/commands/`.
|
|
||||||
|
|
||||||
## Code layout
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Discord status
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
Variables are declared in `src/env.ts`. Without them the widget shows your profile name as offline.
|
|
||||||
|
|
||||||
## Analytics
|
|
||||||
|
|
||||||
Umami is loaded in `src/app.html`.
|
|
||||||
+22
-4
@@ -9,6 +9,9 @@
|
|||||||
{ "number": 6, "name": "portfolio", "path": "/portfolio" },
|
{ "number": 6, "name": "portfolio", "path": "/portfolio" },
|
||||||
{ "number": 7, "name": "models", "path": "/models" }
|
{ "number": 7, "name": "models", "path": "/models" }
|
||||||
],
|
],
|
||||||
|
"iconGroups": [
|
||||||
|
{
|
||||||
|
"name": "pages",
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "label": "about", "icon": "ph:user-circle-duotone", "open": { "page": "/about" } },
|
{ "label": "about", "icon": "ph:user-circle-duotone", "open": { "page": "/about" } },
|
||||||
{ "label": "portfolio", "icon": "ph:briefcase-duotone", "open": { "page": "/portfolio" } },
|
{ "label": "portfolio", "icon": "ph:briefcase-duotone", "open": { "page": "/portfolio" } },
|
||||||
@@ -16,13 +19,28 @@
|
|||||||
{ "label": "hackathons", "icon": "ph:trophy-duotone", "open": { "page": "/projects/hackathons" } },
|
{ "label": "hackathons", "icon": "ph:trophy-duotone", "open": { "page": "/projects/hackathons" } },
|
||||||
{ "label": "blog", "icon": "ph:notebook-duotone", "open": { "page": "/blog" } },
|
{ "label": "blog", "icon": "ph:notebook-duotone", "open": { "page": "/blog" } },
|
||||||
{ "label": "models", "icon": "ph:cube-duotone", "open": { "page": "/models" } },
|
{ "label": "models", "icon": "ph:cube-duotone", "open": { "page": "/models" } },
|
||||||
{ "label": "links", "icon": "ph:qr-code-duotone", "open": { "page": "/links" } },
|
{ "label": "links", "icon": "ph:qr-code-duotone", "open": { "page": "/links" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scripts",
|
||||||
|
"icons": [
|
||||||
{ "label": "kitty", "icon": "ph:terminal-window-duotone", "open": { "terminal": "" } },
|
{ "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": "fastfetch.sh", "icon": "ph:cpu-duotone", "open": { "terminal": "fastfetch" } },
|
||||||
|
{ "label": "whoami.sh", "icon": "ph:identification-card-duotone", "open": { "terminal": "whoami" } },
|
||||||
{ "label": "skills.sh", "icon": "ph:lightning-duotone", "open": { "terminal": "skills" } },
|
{ "label": "skills.sh", "icon": "ph:lightning-duotone", "open": { "terminal": "skills" } },
|
||||||
{ "label": "contact.sh", "icon": "ph:envelope-simple-duotone", "open": { "terminal": "contact" } },
|
{ "label": "contact.sh", "icon": "ph:envelope-simple-duotone", "open": { "terminal": "contact" } }
|
||||||
{ "label": "github", "icon": "ph:github-logo-duotone", "open": { "url": "https://github.com/SirBlobby" } }
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "socials",
|
||||||
|
"icons": [
|
||||||
|
{ "label": "github", "icon": "ph:github-logo-duotone", "open": { "url": "https://github.com/SirBlobby" } },
|
||||||
|
{ "label": "linkedin", "icon": "ph:linkedin-logo-duotone", "open": { "url": "https://www.linkedin.com/in/gmanjunatha/" } },
|
||||||
|
{ "label": "devpost", "icon": "simple-icons:devpost", "open": { "url": "https://devpost.com/Sir_Blob_" } },
|
||||||
|
{ "label": "discord", "icon": "ph:discord-logo-duotone", "open": { "url": "https://discord.com/users/sir_blob_" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"startupCommand": "fastfetch"
|
"startupCommand": "fastfetch"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"role": "Undergraduate Research Assistant, CREATE Lab",
|
"role": "Undergraduate Research Assistant, CREATE Lab",
|
||||||
"organization": "George Mason University, College of Engineering and Computing",
|
"organization": "College of Engineering and Computing",
|
||||||
|
"school": "George Mason University",
|
||||||
"period": "May 2026 – Present",
|
"period": "May 2026 – Present",
|
||||||
"highlights": [
|
"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.",
|
"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.",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"image": "/hacks/learningbuddy.png",
|
"image": "/hacks/learningbuddy.png",
|
||||||
"tags": ["C++", "ESP32-S3", "WebSocket", "MongoDB", "IoT"],
|
"tags": ["C++", "ESP32-S3", "WebSocket", "MongoDB", "IoT"],
|
||||||
"featured": true,
|
"featured": true,
|
||||||
"event": { "name": "HackFax x PatriotHacks 2026", "university": "George Mason University", "location": "Fairfax, VA", "year": "2026" },
|
"event": { "name": "HackFax x PatriotHacks 2026", "year": "2026" },
|
||||||
"awards": [{ "track": "Best use of MongoDB", "place": "Winner" }],
|
"awards": [{ "track": "Best use of MongoDB", "place": "Winner" }],
|
||||||
"links": { "code": "https://github.com/SirBlobby/LearningBuddy", "devpost": "https://devpost.com/software/studybuddy-bc0i3j" }
|
"links": { "code": "https://github.com/SirBlobby/LearningBuddy", "devpost": "https://devpost.com/software/studybuddy-bc0i3j" }
|
||||||
},
|
},
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"image": "/hacks/carbin.png",
|
"image": "/hacks/carbin.png",
|
||||||
"tags": ["AI", "Azure", "CloudConvert", "Python", "React", "TypeScript", "API"],
|
"tags": ["AI", "Azure", "CloudConvert", "Python", "React", "TypeScript", "API"],
|
||||||
"featured": true,
|
"featured": true,
|
||||||
"event": { "name": "PatriotHacks 2024", "university": "George Mason University", "location": "Fairfax, VA", "year": "2024" },
|
"event": { "name": "PatriotHacks 2024", "year": "2024" },
|
||||||
"awards": [
|
"awards": [
|
||||||
{ "track": "Save the World", "place": "2nd Place" },
|
{ "track": "Save the World", "place": "2nd Place" },
|
||||||
{ "track": "Microsoft X Cloudforce", "place": "2nd Place" }
|
{ "track": "Microsoft X Cloudforce", "place": "2nd Place" }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Astroswarm",
|
"name": "Astroswarm",
|
||||||
"organization": "CREATE Lab, George Mason University",
|
"organization": "CREATE Lab",
|
||||||
"period": "May 2026 – Present",
|
"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.",
|
"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"],
|
"tags": ["Python", "ROS", "Swarm Robotics"],
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="text-scale" content="scale" />
|
<meta name="text-scale" content="scale" />
|
||||||
<script>
|
<script>
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
}: { pageOpen: boolean; pageTitle: string; pageIcon: string; pageKey: string; children: Snippet } =
|
}: { pageOpen: boolean; pageTitle: string; pageIcon: string; pageKey: string; children: Snippet } =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
const gapInPixels = 10;
|
|
||||||
const narrowWidth = 768;
|
const narrowWidth = 768;
|
||||||
|
|
||||||
let areaWidth = $state(1280);
|
let areaWidth = $state(1280);
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
|
|
||||||
function styleFor(id: string): string {
|
function styleFor(id: string): string {
|
||||||
const rect = layout.get(id);
|
const rect = layout.get(id);
|
||||||
return rect ? tileStyle(rect, gapInPixels) : 'display: none';
|
return rect ? tileStyle(rect, isNarrow ? 4 : 10) : 'display: none';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -151,12 +150,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.icons-layer :global(.desktop-icons) {
|
.icons-layer {
|
||||||
grid-auto-flow: row;
|
overflow-y: auto;
|
||||||
grid-template-columns: repeat(auto-fill, 112px);
|
|
||||||
grid-template-rows: none;
|
|
||||||
grid-auto-rows: 104px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
import { desktop } from '#lib/content';
|
import { desktop } from '#lib/content';
|
||||||
import { runDesktopAction } from '#lib/desktop/actions';
|
import { runDesktopAction } from '#lib/desktop/actions';
|
||||||
|
|
||||||
|
const icons = desktop.iconGroups.flatMap((group) => group.icons);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="desktop-icons" aria-label="Desktop">
|
<nav class="desktop-icons" aria-label="Desktop">
|
||||||
{#each desktop.icons as item (item.label)}
|
{#each icons as item (item.label)}
|
||||||
<button class="desktop-icon" title={item.label} onclick={() => runDesktopAction(item.open)}>
|
<button class="desktop-icon" title={item.label} onclick={() => runDesktopAction(item.open)}>
|
||||||
<span class="icon-box">
|
<span class="icon-box">
|
||||||
<Icon icon={item.icon} width="36" />
|
<Icon icon={item.icon} width="36" />
|
||||||
@@ -19,9 +21,9 @@
|
|||||||
.desktop-icons {
|
.desktop-icons {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-template-rows: repeat(auto-fill, 104px);
|
grid-template-rows: repeat(auto-fill, 100px);
|
||||||
grid-auto-columns: 112px;
|
grid-auto-columns: 116px;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding: 6px 4px;
|
padding: 6px 2px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
@@ -69,11 +71,12 @@
|
|||||||
.icon-label {
|
.icon-label {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
|
overflow: hidden;
|
||||||
background: color-mix(in srgb, var(--background) 88%, transparent);
|
background: color-mix(in srgb, var(--background) 88%, transparent);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
text-align: center;
|
white-space: nowrap;
|
||||||
word-break: break-word;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-icon:hover .icon-label,
|
.desktop-icon:hover .icon-label,
|
||||||
@@ -81,4 +84,15 @@
|
|||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.desktop-icons {
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-columns: repeat(auto-fill, 104px);
|
||||||
|
grid-template-rows: none;
|
||||||
|
grid-auto-rows: 100px;
|
||||||
|
justify-content: center;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
open: { page: page.path }
|
open: { page: page.path }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const iconEntries: LauncherEntry[] = desktop.icons
|
const iconEntries: LauncherEntry[] = desktop.iconGroups
|
||||||
|
.flatMap((group) => group.icons)
|
||||||
.filter((item) => !('page' in item.open))
|
.filter((item) => !('page' in item.open))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
@@ -172,4 +173,18 @@
|
|||||||
.empty {
|
.empty {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.launcher {
|
||||||
|
top: calc(var(--bar-height) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
max-height: 60dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding: 0.6rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -144,5 +144,17 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.title-bar {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -146,4 +146,11 @@
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 600px) {
|
||||||
|
.canvas-area {
|
||||||
|
height: 280px;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -78,7 +78,12 @@
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.bar {
|
.bar {
|
||||||
font-size: 12px;
|
padding: 0 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar :global(.tray-button) {
|
||||||
|
width: 36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -20,13 +20,15 @@
|
|||||||
|
|
||||||
<span class="clock full">{now ? formatClock(now) : ''}</span>
|
<span class="clock full">{now ? formatClock(now) : ''}</span>
|
||||||
<span class="clock short">{now ? formatClock(now).split(' ').slice(1).join(' ') : ''}</span>
|
<span class="clock short">{now ? formatClock(now).split(' ').slice(1).join(' ') : ''}</span>
|
||||||
|
<span class="clock time-only">{now ? formatClock(now).split(' ').at(-1) : ''}</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.clock {
|
.clock {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.short {
|
.short,
|
||||||
|
.time-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,4 +41,14 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.short {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-only {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -55,4 +55,14 @@
|
|||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--bar-height) + 6px);
|
||||||
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
width: 20px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
['Name', profile.displayName],
|
['Name', profile.displayName],
|
||||||
['Title', profile.title],
|
['Title', profile.title],
|
||||||
['Uptime', `${uptimeDays} days`],
|
['Uptime', `${uptimeDays} days`],
|
||||||
['School', profile.education.school],
|
|
||||||
['Languages', (profile.skills.Languages ?? []).slice(0, 5).join(', ')]
|
['Languages', (profile.skills.Languages ?? []).slice(0, 5).join(', ')]
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
.terminal {
|
.terminal {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
padding: 0.75rem 1.1rem 1.5rem;
|
padding: 0.75rem 1.1rem 1.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
@@ -185,4 +186,15 @@
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
caret-color: var(--foreground);
|
caret-color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.terminal {
|
||||||
|
padding: 0.6rem 0.7rem 1.25rem;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -45,4 +45,18 @@
|
|||||||
p {
|
p {
|
||||||
margin: 0.5rem 0 0;
|
margin: 0.5rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 520px) {
|
||||||
|
.profile-intro {
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -34,6 +34,7 @@ export interface LabeledLink {
|
|||||||
export interface Experience {
|
export interface Experience {
|
||||||
role: string;
|
role: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
school?: string;
|
||||||
employmentType?: string;
|
employmentType?: string;
|
||||||
period: string;
|
period: string;
|
||||||
highlights: string[];
|
highlights: string[];
|
||||||
@@ -128,6 +129,11 @@ export interface Model3D {
|
|||||||
|
|
||||||
export type DesktopAction = { page: string } | { terminal: string } | { url: string };
|
export type DesktopAction = { page: string } | { terminal: string } | { url: string };
|
||||||
|
|
||||||
|
export interface DesktopIconGroup {
|
||||||
|
name: string;
|
||||||
|
icons: DesktopIcon[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DesktopIcon {
|
export interface DesktopIcon {
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -143,7 +149,7 @@ export interface Workspace {
|
|||||||
export interface DesktopContent {
|
export interface DesktopContent {
|
||||||
defaultTheme: string;
|
defaultTheme: string;
|
||||||
workspaces: Workspace[];
|
workspaces: Workspace[];
|
||||||
icons: DesktopIcon[];
|
iconGroups: DesktopIconGroup[];
|
||||||
startupCommand: string;
|
startupCommand: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ function whoamiOutput(): OutputBlock[] {
|
|||||||
line(span('─'.repeat(header.length), 'muted')),
|
line(span('─'.repeat(header.length), 'muted')),
|
||||||
field('alias', profile.displayName),
|
field('alias', profile.displayName),
|
||||||
field('role', profile.title),
|
field('role', profile.title),
|
||||||
field('school', `${profile.education.school}, ${profile.education.degree}`),
|
|
||||||
...(currentRole ? [field('now', span(currentRole.role, 'orange'), span(' @ ', 'muted'), currentRole.organization)] : []),
|
...(currentRole ? [field('now', span(currentRole.role, 'orange'), span(' @ ', 'muted'), currentRole.organization)] : []),
|
||||||
...(interests.length ? [field('interests', ...joinWithDots(interests.map((interest) => span(interest, 'magenta'))))] : []),
|
...(interests.length ? [field('interests', ...joinWithDots(interests.map((interest) => span(interest, 'magenta'))))] : []),
|
||||||
blankLine,
|
blankLine,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
background-color: var(--dark-background);
|
background-color: var(--dark-background);
|
||||||
background-image: var(--wallpaper), radial-gradient(circle at 70% 30%, var(--lighter-background), var(--dark-background));
|
background-image: var(--wallpaper), radial-gradient(circle at 70% 30%, var(--lighter-background), var(--dark-background));
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -94,3 +96,19 @@ button {
|
|||||||
.muted {
|
.muted {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--gaps-out: 6px;
|
||||||
|
--border-size: 2px;
|
||||||
|
--bar-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding: 0.6rem 0.85rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<article class="entry">
|
<article class="entry">
|
||||||
<h3>{job.role}</h3>
|
<h3>{job.role}</h3>
|
||||||
<div class="organization">
|
<div class="organization">
|
||||||
{job.organization}{#if job.employmentType}<span class="muted"> · {job.employmentType}</span>{/if}
|
{#if job.school}{job.school}, {/if}{job.organization}{#if job.employmentType}<span class="muted"> · {job.employmentType}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="muted">{job.period}</div>
|
<div class="muted">{job.period}</div>
|
||||||
{#each job.awards ?? [] as award (award)}
|
{#each job.awards ?? [] as award (award)}
|
||||||
|
|||||||
Reference in new issue
Block a user