swap friend SVGs for wizard emoji, 2-up shorter project cards on phones

This commit is contained in:
Martin Dvořák 2026-09-30 21:20:05 +00:00
commit 6827ca6ed1
9 changed files with 41 additions and 19815 deletions

View file

@ -129,11 +129,11 @@ Themes: `web` = default frosted-glass website (sidebar + content, light/dark),
content column of `.glass` panels: hero (with a compact "Find me" socials row), content column of `.glass` panels: hero (with a compact "Find me" socials row),
a `.web-quick` 2-up ("Right now" + "Gym PRs"), then services, activity, a `.web-quick` 2-up ("Right now" + "Gym PRs"), then services, activity,
projects, hardware, features (Revival/RustyWolf) and friends. Friend cards are projects, hardware, features (Revival/RustyWolf) and friends. Friend cards are
**text-only** (name + domain — the web theme does not render `Friend.img` **text-only** (name + domain) in a `.friends-grid` that collapses to a 2×2
avatars) in a `.friends-grid` that collapses to a 2×2 grid on phones. grid on phones; the desktop friend icons use a 🧙 emoji glyph (the old
Projects/hardware per-friend SVGs are gone). Projects/hardware use a CSS multi-column
use a CSS multi-column **masonry** gallery (`.project-grid`) so short cards **masonry** gallery (`.project-grid`) so short cards don't leave half-empty
don't leave half-empty grid slots. The two feature panels (`.web-split`) stretch grid slots; on phones it drops to 2 columns with a shorter card image. The two feature panels (`.web-split`) stretch
to equal height and share a fixed `16 / 10` `.web-shot` frame with to equal height and share a fixed `16 / 10` `.web-shot` frame with
`object-fit: cover` (+ `object-position: center top`), so source images with `object-fit: cover` (+ `object-position: center top`), so source images with
different aspect ratios still line up. The old stacked TUI panel layout and the different aspect ratios still line up. The old stacked TUI panel layout and the
@ -245,12 +245,11 @@ with icons, it's almost certainly here.
exceeded" (this bug has already happened once — see history below). exceeded" (this bug has already happened once — see history below).
### Gotcha: native drag hijacks the pointer gesture ### Gotcha: native drag hijacks the pointer gesture
Icons whose glyph is an `<img>` (the friend shortcuts that carry `Friend.img`, Icons whose glyph is an `<img>` can start the browser's **native image drag**,
e.g. `/skopec.svg`, `/bukovina.svg`, `/vlcak.svg`) can start the browser's which fires `pointercancel` and kills the custom drag, while emoji-based icons
**native image drag**, which fires `pointercancel` and kills the custom drag. are unaffected (looks like "only some icons don't drag"). No desktop icon uses
Emoji-based icons are unaffected — including a friend with no `img` (`Friend.img` an `<img>` glyph now — the friends are 🧙 emoji like the rest — but the guard
is optional now; it falls back to the 🫂 glyph), which makes this look like below is kept in case an image glyph is reintroduced.
"only some icons don't drag".
Guard (already in place, keep it that way): Guard (already in place, keep it that way):
- `draggable={false}` on the `<Image>` in `renderIcon`. - `draggable={false}` on the `<Image>` in `renderIcon`.
@ -381,8 +380,8 @@ knows about icon ids, via `APP_BY_ID`.
two differ. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send two differ. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send
no framing headers, so they frame the live site. `skopec.dev` sends no framing headers, so they frame the live site. `skopec.dev` sends
`X-Frame-Options: SAMEORIGIN` on every path, so `friend.proxy` routes it `X-Frame-Options: SAMEORIGIN` on every path, so `friend.proxy` routes it
through `/api/frame`. `Friend.img` is optional and only feeds the desktop icon through `/api/frame`. Friend icons are a 🧙 emoji everywhere (no per-friend
/ FriendsApp avatar — the web theme's friends section is text-only. images); the web theme's friends section is text-only.
- `/api/frame` is a **host-allowlisted** header-stripping proxy for sites that - `/api/frame` is a **host-allowlisted** header-stripping proxy for sites that
refuse framing. It fetches the HTML server-side (so the upstream refuse framing. It fetches the HTML server-side (so the upstream
`X-Frame-Options` never reaches the browser), injects `<base href="origin/">` `X-Frame-Options` never reaches the browser), injects `<base href="origin/">`
@ -412,7 +411,7 @@ knows about icon ids, via `APP_BY_ID`.
Theme + scheme are read pre-paint from `localStorage.theme` / `.scheme`, so a Theme + scheme are read pre-paint from `localStorage.theme` / `.scheme`, so a
quick check is to set those in devtools and reload. quick check is to set those in devtools and reload.
- `next/image` is lazy by default, so a headless full-page screenshot shows the - `next/image` is lazy by default, so a headless full-page screenshot shows the
below-the-fold feature shots and friend avatars as blank boxes. Scroll the page below-the-fold feature shots as blank boxes. Scroll the page
slowly first and they load; this is a capture artifact, not a bug. Headless slowly first and they load; this is a capture artifact, not a bug. Headless
Chromium lives at `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome` Chromium lives at `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome`
(drive it with `--headless=new --remote-debugging-port=…` and CDP). (drive it with `--headless=new --remote-debugging-port=…` and CDP).

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 285 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 566 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 351 KiB

View file

@ -76,8 +76,7 @@ const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({
: f.url, : f.url,
embed: f.iframe, embed: f.iframe,
color: "#c0392b", color: "#c0392b",
icon: "🫂", icon: "🧙",
image: f.img,
desc: f.desc, desc: f.desc,
})); }));
@ -363,8 +362,7 @@ export default function WindowsDesktop() {
id: f.id, id: f.id,
label: f.label, label: f.label,
title: f.label, title: f.label,
glyph: "🫂", glyph: "🧙",
image: f.image,
onOpen: () => openShortcut(f), onOpen: () => openShortcut(f),
})); }));
const allEntries = [...leftEntries, ...serviceEntries, ...friendEntries]; const allEntries = [...leftEntries, ...serviceEntries, ...friendEntries];
@ -505,17 +503,7 @@ export default function WindowsDesktop() {
className="kickoff-item" className="kickoff-item"
onClick={() => openShortcut(f)} onClick={() => openShortcut(f)}
> >
<span className="kickoff-icon"> <span className="kickoff-icon">{f.icon}</span>
{f.image && (
<Image
src={f.image}
alt={f.label}
width={20}
height={20}
className="kickoff-icon-img"
/>
)}
</span>
<span className="kickoff-label">{f.label}</span> <span className="kickoff-label">{f.label}</span>
</button> </button>
))} ))}

View file

@ -255,20 +255,7 @@ function FriendsApp() {
key={idx} key={idx}
url={friend.realUrl || friend.url} url={friend.realUrl || friend.url}
className="friend" className="friend"
icon={ icon="🧙"
friend.img ? (
<Image
src={friend.img}
alt={friend.name}
width={68}
height={68}
className="friend-icon"
style={{ borderRadius: "50%" }}
/>
) : (
"🫂"
)
}
name={friend.name} name={friend.name}
desc={friend.desc} desc={friend.desc}
/> />

View file

@ -23,12 +23,6 @@ export interface Friend {
url: string; url: string;
/** The live site: the window's `www` button and the status check use this. */ /** The live site: the window's `www` button and the status check use this. */
realUrl: string; realUrl: string;
/**
* Desktop-icon avatar. Optional: the web theme's friends section is
* text-only (no SVG avatars), and a friend without one falls back to the
* generic glyph on the desktop.
*/
img?: string;
desc: string; desc: string;
/** Render in an iframe in a desktop window. */ /** Render in an iframe in a desktop window. */
iframe: boolean; iframe: boolean;
@ -250,7 +244,6 @@ export const friends: Friend[] = [
// window loads the live site through /api/frame (see `proxy` below). // window loads the live site through /api/frame (see `proxy` below).
url: "https://skopec.dev/", url: "https://skopec.dev/",
realUrl: "https://skopec.dev/", realUrl: "https://skopec.dev/",
img: "/skopec.svg",
desc: "skopec.dev", desc: "skopec.dev",
iframe: true, iframe: true,
proxy: true, proxy: true,
@ -259,7 +252,6 @@ export const friends: Friend[] = [
name: "Filip Bukovina", name: "Filip Bukovina",
url: "https://bukovinafilip.com/", url: "https://bukovinafilip.com/",
realUrl: "https://bukovinafilip.com/", realUrl: "https://bukovinafilip.com/",
img: "/bukovina.svg",
desc: "bukovinafilip.com", desc: "bukovinafilip.com",
iframe: true, iframe: true,
}, },
@ -267,7 +259,6 @@ export const friends: Friend[] = [
name: "Tomáš Vlk", name: "Tomáš Vlk",
url: "https://vlcak.me", url: "https://vlcak.me",
realUrl: "https://vlcak.me", realUrl: "https://vlcak.me",
img: "/vlcak.svg",
desc: "vlcak.me", desc: "vlcak.me",
iframe: true, iframe: true,
}, },

View file

@ -82,8 +82,8 @@ html[data-theme="skeuo"] body {
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
transition: background-color 0.12s ease, border-color 0.12s ease; transition: background-color 0.12s ease, border-color 0.12s ease;
/* Icons are dragged by our own pointer handlers. Without this, the browser's /* Icons are dragged by our own pointer handlers. Without this, the browser's
native image/link drag hijacks the gesture (it fires pointercancel), which native image/link drag hijacks the gesture (it fires pointercancel) for any
broke dragging for icons whose glyph is an <img> - the friend shortcuts. */ icon whose glyph is an <img>; kept in case one is reintroduced. */
-webkit-user-drag: none; -webkit-user-drag: none;
user-select: none; user-select: none;
} }
@ -1182,10 +1182,6 @@ a.win-icon {
.win-body .service-button:hover .service-desc { .win-body .service-button:hover .service-desc {
color: #d3dae3; color: #d3dae3;
} }
.win-body .friend-icon {
filter: invert(1) brightness(1.5);
}
.win-body .project-card { .win-body .project-card {
border-color: #1b1e20; border-color: #1b1e20;
background: linear-gradient(to bottom, #31363b, #2a2e32); background: linear-gradient(to bottom, #31363b, #2a2e32);

View file

@ -562,7 +562,8 @@ html[data-theme="web"] body {
color: var(--fg); color: var(--fg);
} }
/* Friend cards stack their round avatar above the name. */ /* Friend cards centre their text (the web theme has no icon; the desktop
Friends app shows the wizard emoji above the name). */
.service-button.friend { .service-button.friend {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
@ -592,23 +593,13 @@ html[data-theme="web"] body {
transform: none; transform: none;
filter: none; filter: none;
} }
/* The desktop Friends app shows a big wizard emoji in place of the old avatars. */
.service-button.friend .service-icon { .service-button.friend .service-icon {
width: 64px; width: 64px;
height: 64px; height: 64px;
border-radius: 50%; font-size: 2.6rem;
overflow: hidden;
filter: none; filter: none;
} }
.friend-icon {
display: block;
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
html[data-theme="web"][data-scheme="dark"] .friend-icon {
filter: invert(1) brightness(1.5);
}
/* ============================================================ /* ============================================================
PROJECTS / HARDWARE PROJECTS / HARDWARE
@ -1129,8 +1120,25 @@ html[data-theme="web"][data-scheme="dark"] .heatmap-legend-cell[data-level="4"]
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 600; font-weight: 600;
} }
/* Projects/hardware gallery: 2-up on phones instead of one full-width card
per row, with a shorter image + tighter card so a row is compact. */
.project-grid { .project-grid {
columns: 1; columns: 2;
column-gap: 10px;
}
.project-card {
margin-bottom: 10px;
padding: 10px;
}
.project-image {
height: 90px;
margin-bottom: 8px;
}
.project-title {
font-size: 0.88rem;
}
.project-desc {
font-size: 0.75rem;
} }
.services-buttons, .services-buttons,
.services-buttons.services-main { .services-buttons.services-main {