diff --git a/README.md b/README.md index 4167b53..0b5b541 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ more. A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card instead. - **Friend sites in windows** — clicking a friend's desktop icon opens their site in a - window. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they load directly; - `skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a + window. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send no framing headers, + so they load directly; `skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a header-stripping proxy that serves the live page from our origin. The `www` button always opens the real site. - **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on diff --git a/agents.md b/agents.md index 2b57c65..81e1b49 100644 --- a/agents.md +++ b/agents.md @@ -128,7 +128,10 @@ Themes: `web` = default frosted-glass website (sidebar + content, light/dark), section nav, light/dark switch, desktop switch, contact/clock) beside a single 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, - projects, hardware, features (Revival/RustyWolf) and friends. Projects/hardware + projects, hardware, features (Revival/RustyWolf) and friends. Friend cards are + **text-only** (name + domain — the web theme does not render `Friend.img` + avatars) in a `.friends-grid` that collapses to a 2×2 grid on phones. + Projects/hardware use a CSS multi-column **masonry** gallery (`.project-grid`) so short cards don't leave half-empty grid slots. The two feature panels (`.web-split`) stretch to equal height and share a fixed `16 / 10` `.web-shot` frame with @@ -242,11 +245,12 @@ with icons, it's almost certainly here. exceeded" (this bug has already happened once — see history below). ### Gotcha: native drag hijacks the pointer gesture -Icons whose glyph is an `` (the friend shortcuts, which use `next/image` -with `/skopec.svg`, `/bukovina.svg`, `/vlcak.svg`) can start the browser's +Icons whose glyph is an `` (the friend shortcuts that carry `Friend.img`, +e.g. `/skopec.svg`, `/bukovina.svg`, `/vlcak.svg`) can start the browser's **native image drag**, which fires `pointercancel` and kills the custom drag. -Emoji-based icons are unaffected, which makes this look like "only some icons -don't drag". +Emoji-based icons are unaffected — including a friend with no `img` (`Friend.img` +is optional now; it falls back to the 🫂 glyph), which makes this look like +"only some icons don't drag". Guard (already in place, keep it that way): - `draggable={false}` on the `` in `renderIcon`. @@ -374,9 +378,11 @@ knows about icon ids, via `APP_BY_ID`. - Friend shortcuts reuse the same embed path, driven by `data.ts`. `friend.iframe` is the `embed` flag; `friend.url` is what the frame loads and `friend.realUrl` is what the www button opens — `createWebsiteApp`'s `embedUrl` option lets the - two differ. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they - frame the live site. `skopec.dev` sends `X-Frame-Options: SAMEORIGIN` on every - path, so `friend.proxy` routes it through `/api/frame`. + two differ. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send + no framing headers, so they frame the live site. `skopec.dev` sends + `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 + / FriendsApp avatar — the web theme's friends section is text-only. - `/api/frame` is a **host-allowlisted** header-stripping proxy for sites that refuse framing. It fetches the HTML server-side (so the upstream `X-Frame-Options` never reaches the browser), injects `` diff --git a/src/app/api/ping/route.ts b/src/app/api/ping/route.ts index 0bc19d4..7d4d4c6 100644 --- a/src/app/api/ping/route.ts +++ b/src/app/api/ping/route.ts @@ -15,6 +15,7 @@ const ALLOWED_HOSTS = [ 'skopec.dev', 'bukovinafilip.com', 'vlcak.me', + 'k0t4tkos-portfolio.com', ]; function isAllowedHost(hostname: string): boolean { diff --git a/src/app/components/ServiceButton.tsx b/src/app/components/ServiceButton.tsx index 64767d7..73b9897 100644 --- a/src/app/components/ServiceButton.tsx +++ b/src/app/components/ServiceButton.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, ReactNode } from 'react'; -export default function ServiceButton({ url, checkUrl, className, icon, name, desc, access, disabled }: { url: string, checkUrl?: string, className: string, icon: ReactNode, name: string, desc: string, access?: 'public' | 'private' | 'on_request', disabled?: boolean }) { +export default function ServiceButton({ url, checkUrl, className, icon, name, desc, access, disabled }: { url: string, checkUrl?: string, className: string, icon?: ReactNode, name: string, desc: string, access?: 'public' | 'private' | 'on_request', disabled?: boolean }) { const [status, setStatus] = useState<'pending' | 'up' | 'down'>('pending'); useEffect(() => { @@ -30,10 +30,16 @@ export default function ServiceButton({ url, checkUrl, className, icon, name, de const content = ( <> - + {icon != null ? ( + + + {icon} + + ) : ( + // No icon (e.g. the web theme's text-only friend cards): the + // status dot becomes a direct child so it anchors to the button. - {icon} - + )} {name} diff --git a/src/app/components/WebLayout.tsx b/src/app/components/WebLayout.tsx index fdfaff3..672061a 100644 --- a/src/app/components/WebLayout.tsx +++ b/src/app/components/WebLayout.tsx @@ -332,22 +332,12 @@ export default function WebLayout() {
-
+
{friends.map((friend, idx) => ( - } name={friend.name} desc={friend.desc} /> diff --git a/src/app/components/apps.tsx b/src/app/components/apps.tsx index 71dc8ac..1a9a726 100644 --- a/src/app/components/apps.tsx +++ b/src/app/components/apps.tsx @@ -256,14 +256,18 @@ function FriendsApp() { url={friend.realUrl || friend.url} className="friend" icon={ - {friend.name} + friend.img ? ( + {friend.name} + ) : ( + "🫂" + ) } name={friend.name} desc={friend.desc} diff --git a/src/app/data.ts b/src/app/data.ts index 242dd52..db31f33 100644 --- a/src/app/data.ts +++ b/src/app/data.ts @@ -23,7 +23,12 @@ export interface Friend { url: string; /** The live site: the window's `www` button and the status check use this. */ realUrl: string; - img: 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; /** Render in an iframe in a desktop window. */ iframe: boolean; @@ -266,6 +271,14 @@ export const friends: Friend[] = [ desc: "vlcak.me", iframe: true, }, + { + name: "Danny Koťan", + // No X-Frame-Options / frame-ancestors, so it frames directly. + url: "https://k0t4tkos-portfolio.com/", + realUrl: "https://k0t4tkos-portfolio.com/", + desc: "k0t4tkos-portfolio.com", + iframe: true, + }, ]; export const WALLPAPER = "/wallpaper.jpg"; diff --git a/src/app/web.css b/src/app/web.css index 54fdf36..466b4d3 100644 --- a/src/app/web.css +++ b/src/app/web.css @@ -571,10 +571,18 @@ html[data-theme="web"] body { } .service-button.friend .service-text { align-items: center; + width: 100%; + /* Leave room for the corner status dot so names don't run under it. */ + padding-inline: 10px; +} +/* Friend cards are text-only now, so let long names wrap instead of running + under the corner status dot. */ +.service-button.friend .service-name { + white-space: normal; } .service-button.friend .service-status-dot { - top: 14px; - right: 14px; + top: 12px; + right: 12px; left: auto; width: 11px; height: 11px; @@ -1128,6 +1136,10 @@ html[data-theme="web"][data-scheme="dark"] .heatmap-legend-cell[data-level="4"] .services-buttons.services-main { grid-template-columns: 1fr; } + /* Friends are text-only cards now, so they read fine as a 2x2 grid. */ + .services-buttons.friends-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } .social-buttons { grid-template-columns: 1fr 1fr; }