add Danny Kotan, make web friend cards text-only 2x2 on phones
This commit is contained in:
parent
9dcc7da133
commit
dbf193fde9
8 changed files with 68 additions and 36 deletions
|
|
@ -50,8 +50,8 @@ more.
|
||||||
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
|
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
|
||||||
instead.
|
instead.
|
||||||
- **Friend sites in windows** — clicking a friend's desktop icon opens their site in a
|
- **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;
|
window. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send no framing headers,
|
||||||
`skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a
|
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
|
header-stripping proxy that serves the live page from our origin. The `www` button always
|
||||||
opens the real site.
|
opens the real site.
|
||||||
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
|
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
|
||||||
|
|
|
||||||
22
agents.md
22
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
|
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),
|
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. 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
|
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
|
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
|
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).
|
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, which use `next/image`
|
Icons whose glyph is an `<img>` (the friend shortcuts that carry `Friend.img`,
|
||||||
with `/skopec.svg`, `/bukovina.svg`, `/vlcak.svg`) can start the browser's
|
e.g. `/skopec.svg`, `/bukovina.svg`, `/vlcak.svg`) can start the browser's
|
||||||
**native image drag**, which fires `pointercancel` and kills the custom drag.
|
**native image drag**, which fires `pointercancel` and kills the custom drag.
|
||||||
Emoji-based icons are unaffected, which makes this look like "only some icons
|
Emoji-based icons are unaffected — including a friend with no `img` (`Friend.img`
|
||||||
don't drag".
|
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):
|
Guard (already in place, keep it that way):
|
||||||
- `draggable={false}` on the `<Image>` in `renderIcon`.
|
- `draggable={false}` on the `<Image>` 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`
|
- 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 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
|
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
|
two differ. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send
|
||||||
frame the live site. `skopec.dev` sends `X-Frame-Options: SAMEORIGIN` on every
|
no framing headers, so they frame the live site. `skopec.dev` sends
|
||||||
path, so `friend.proxy` routes it through `/api/frame`.
|
`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
|
- `/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/">`
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const ALLOWED_HOSTS = [
|
||||||
'skopec.dev',
|
'skopec.dev',
|
||||||
'bukovinafilip.com',
|
'bukovinafilip.com',
|
||||||
'vlcak.me',
|
'vlcak.me',
|
||||||
|
'k0t4tkos-portfolio.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isAllowedHost(hostname: string): boolean {
|
function isAllowedHost(hostname: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { useEffect, useState, ReactNode } from 'react';
|
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');
|
const [status, setStatus] = useState<'pending' | 'up' | 'down'>('pending');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -30,10 +30,16 @@ export default function ServiceButton({ url, checkUrl, className, icon, name, de
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
{icon != null ? (
|
||||||
<span className="service-icon-wrap">
|
<span className="service-icon-wrap">
|
||||||
<span className="service-status-dot" />
|
<span className="service-status-dot" />
|
||||||
<span className="service-icon">{icon}</span>
|
<span className="service-icon">{icon}</span>
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
// 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.
|
||||||
|
<span className="service-status-dot" />
|
||||||
|
)}
|
||||||
<span className="service-text">
|
<span className="service-text">
|
||||||
<span className="service-name">
|
<span className="service-name">
|
||||||
{name}
|
{name}
|
||||||
|
|
|
||||||
|
|
@ -332,22 +332,12 @@ export default function WebLayout() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section id="friends" eyebrow="Links" title="Friend's websites and projects">
|
<Section id="friends" eyebrow="Links" title="Friend's websites and projects">
|
||||||
<div className="services-buttons">
|
<div className="services-buttons friends-grid">
|
||||||
{friends.map((friend, idx) => (
|
{friends.map((friend, idx) => (
|
||||||
<ServiceButton
|
<ServiceButton
|
||||||
key={idx}
|
key={idx}
|
||||||
url={friend.realUrl || friend.url}
|
url={friend.realUrl || friend.url}
|
||||||
className="friend"
|
className="friend"
|
||||||
icon={
|
|
||||||
<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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ function FriendsApp() {
|
||||||
url={friend.realUrl || friend.url}
|
url={friend.realUrl || friend.url}
|
||||||
className="friend"
|
className="friend"
|
||||||
icon={
|
icon={
|
||||||
|
friend.img ? (
|
||||||
<Image
|
<Image
|
||||||
src={friend.img}
|
src={friend.img}
|
||||||
alt={friend.name}
|
alt={friend.name}
|
||||||
|
|
@ -264,6 +265,9 @@ function FriendsApp() {
|
||||||
className="friend-icon"
|
className="friend-icon"
|
||||||
style={{ borderRadius: "50%" }}
|
style={{ borderRadius: "50%" }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
"🫂"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
name={friend.name}
|
name={friend.name}
|
||||||
desc={friend.desc}
|
desc={friend.desc}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ 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;
|
||||||
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;
|
desc: string;
|
||||||
/** Render in an iframe in a desktop window. */
|
/** Render in an iframe in a desktop window. */
|
||||||
iframe: boolean;
|
iframe: boolean;
|
||||||
|
|
@ -266,6 +271,14 @@ export const friends: Friend[] = [
|
||||||
desc: "vlcak.me",
|
desc: "vlcak.me",
|
||||||
iframe: true,
|
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";
|
export const WALLPAPER = "/wallpaper.jpg";
|
||||||
|
|
|
||||||
|
|
@ -571,10 +571,18 @@ html[data-theme="web"] body {
|
||||||
}
|
}
|
||||||
.service-button.friend .service-text {
|
.service-button.friend .service-text {
|
||||||
align-items: center;
|
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 {
|
.service-button.friend .service-status-dot {
|
||||||
top: 14px;
|
top: 12px;
|
||||||
right: 14px;
|
right: 12px;
|
||||||
left: auto;
|
left: auto;
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
|
|
@ -1128,6 +1136,10 @@ html[data-theme="web"][data-scheme="dark"] .heatmap-legend-cell[data-level="4"]
|
||||||
.services-buttons.services-main {
|
.services-buttons.services-main {
|
||||||
grid-template-columns: 1fr;
|
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 {
|
.social-buttons {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue