rebuild the web theme as a physical desk (walnut surface, monitor + keyboard, real rack, drive drawer, paper tags, dot-matrix report, brass plaques)
This commit is contained in:
parent
dd6aeb5486
commit
cfc256c7bd
9 changed files with 1604 additions and 869 deletions
36
README.md
36
README.md
|
|
@ -2,15 +2,16 @@
|
|||
|
||||
> The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**.
|
||||
|
||||
It's a **skeuomorphic personal site**: a dark homelab desk built from anodised aluminium, OLED
|
||||
glass, carbon fibre and paper, with the whole KDE desktop simulation kept behind a physical
|
||||
power switch. Two modes, one content set:
|
||||
It's a **skeuomorphic personal site**: a walnut desk with graphite hardware, cream paper and
|
||||
brass details resting on it, with the whole KDE desktop simulation kept behind a physical power
|
||||
switch. Two modes, one content set:
|
||||
|
||||
- **Web** (default) — the homelab desk. A 4U rack carries the headline on an engraved nameplate
|
||||
and a live console readout; services are rack units with status LEDs, the 18 coding projects
|
||||
are labelled SSD caddies in a drive drawer, hardware hangs on a pegboard, the contribution
|
||||
heatmap is an LED matrix and the socials are a patch panel. Textures are CC0 — see
|
||||
[`/credits`](http://localhost:3000/credits).
|
||||
- **Web** (default) — the desk. A monitor carries the headline with a keyboard in front of it
|
||||
and a small rack beside it; services are 1U rack units with health LEDs, the 18 coding
|
||||
projects are labelled drives in a drawer, the machines hang as paper tags on a pegboard, the
|
||||
contribution heatmap is printed on continuous dot-matrix paper, friends are business cards,
|
||||
the gym log is a clipboard and the socials are a patch panel. Textures are CC0 — see
|
||||
[`/credits`](https://dvorakmortonxd.dev/credits).
|
||||
- **Desktop mode** — the KDE Plasma / Windows-style **desktop simulation**: draggable
|
||||
snap-to-grid icons, resizable/floating windows, a Start menu, taskbar, live clock and a
|
||||
working in-browser terminal (Alacritty). Reached from the power switch and hidden on phones.
|
||||
|
|
@ -23,8 +24,9 @@ heatmap**, gym SBD and links to friends' sites.
|
|||
|
||||
- **Web theme is the landing view** — the site always opens on the desk; the theme is
|
||||
deliberately not persisted, so a reload never drops you into the desktop.
|
||||
- **Skeuomorphic hardware** — rack, LEDs, rocker switch, screws, vents and panels are CSS
|
||||
materials over five CC0 textures; every section is a real-world object rather than a card.
|
||||
- **Skeuomorphic materials** — walnut desk, graphite faceplates, cream paper, brass plaques and
|
||||
amber LEDs, all CSS over five CC0 textures. Every section is a physical object with a contact
|
||||
shadow; nothing is pure white on black or grey on black.
|
||||
- **Desktop mode (desktop-only)** — a physical power switch in the top rail (and in the footer)
|
||||
flips to the KDE-style desktop for the session. Phones never get it: the switch is hidden, so
|
||||
the old iOS-style phone home screen is gone.
|
||||
|
|
@ -64,15 +66,15 @@ heatmap**, gym SBD and links to friends' sites.
|
|||
- **Framework:** Next.js 16 (App Router, Turbopack)
|
||||
- **Language:** TypeScript, React 19 (client components everywhere interactivity lives)
|
||||
- **Styling:** `src/app/globals.css` holds the palette tokens, base styles, the Tailwind v4
|
||||
`@theme`/`@utility` classes and the scroll reveals. `src/app/web.css` is the skeuomorphic
|
||||
material system (metal, OLED glass, paper, carbon, rubber, plastic, LEDs, screws, vents,
|
||||
switches) and the object layouts. `page.css` keeps the shared component styles the desktop
|
||||
apps still use, and `skeuo.css` is the KDE desktop.
|
||||
`@theme`/`@utility` classes and the scroll reveals. `src/app/web.css` is the material system
|
||||
(walnut desk, graphite metal, cream paper, brass, OLED glass, LEDs, screws, rack rails, rocker
|
||||
switch) and the object layouts. `page.css` keeps the shared component styles the desktop apps
|
||||
still use, and `skeuo.css` is the KDE desktop.
|
||||
- **Fonts:** Newsreader (headlines, paper) + IBM Plex Sans/Mono (body, machine labels) via
|
||||
`next/font/google`; the desktop keeps its system sans stack.
|
||||
- **Textures:** five CC0 materials from ambientCG, downscaled to 512 px in `public/textures`.
|
||||
Attribution is on `/credits`.
|
||||
- **Version:** `3.1.0` (see `package.json`)
|
||||
- **Textures:** five CC0 materials from ambientCG — `desk` (walnut), `metal`, `paper`, `carbon`,
|
||||
`plastic` — downscaled to 512 px in `public/textures`. Attribution is on `/credits`.
|
||||
- **Version:** `3.2.0` (see `package.json`)
|
||||
|
||||
## 🚀 Running Locally
|
||||
|
||||
|
|
|
|||
30
agents.md
30
agents.md
|
|
@ -34,19 +34,20 @@ When in doubt, prefer updating. A stale README **or** agents.md is treated as a
|
|||
## What this project is
|
||||
|
||||
Personal portfolio site for **dvorakmortonxd.dev**. The default view is a
|
||||
**skeuomorphic homelab desk** (a rack, LEDs, paper, anodised metal); a physical
|
||||
power switch flips to a **KDE Plasma desktop simulation**. Phones only ever get
|
||||
the desk.
|
||||
**skeuomorphic desk**: a walnut surface with a monitor, keyboard, rack, drive
|
||||
drawer, pegboard, dot-matrix report, business cards, clipboard and patch panel
|
||||
resting on it. A physical power switch flips to a **KDE Plasma desktop
|
||||
simulation**. Phones only ever get the desk.
|
||||
|
||||
- Framework: **Next.js 16** (App Router, Turbopack), **React 19**, TypeScript.
|
||||
- Styling: **Tailwind v4** for layout plus **`web.css`**, the skeuomorphic
|
||||
material system (metal, OLED glass, paper, carbon, rubber, plastic, LEDs,
|
||||
screws, vents, switches) over five CC0 textures in `public/textures`
|
||||
- Styling: **Tailwind v4** for layout plus **`web.css`**, the material system
|
||||
(walnut desk, graphite metal, cream paper, brass, OLED glass, LEDs, screws,
|
||||
rack rails, rocker switch) over five CC0 textures in `public/textures`
|
||||
(credited on `/credits`). `globals.css` holds the tokens, base styles and the
|
||||
reveals; `skeuo.css` and `page.css` are the desktop and the shared
|
||||
desktop-app styles.
|
||||
- Path alias: `@/*` → `src/*`.
|
||||
- Version: `3.1.0` (see `package.json`).
|
||||
- Version: `3.2.0` (see `package.json`).
|
||||
|
||||
### Scripts
|
||||
```bash
|
||||
|
|
@ -123,11 +124,11 @@ scripts/
|
|||
desktop is up so its scroll position survives; the desktop mounts on demand
|
||||
(phones can never reach `skeuo`).
|
||||
|
||||
Modes: `web` = the skeuomorphic homelab desk (default/landing), `skeuo` = KDE
|
||||
Plasma desktop.
|
||||
Modes: `web` = the skeuomorphic desk (default/landing), `skeuo` = KDE Plasma
|
||||
desktop.
|
||||
|
||||
The desk uses **five CC0 textures** from ambientCG (`public/textures`: metal,
|
||||
paper, carbon, rubber, plastic), downscaled to 512 px and credited on
|
||||
The desk uses **five CC0 textures** from ambientCG (`public/textures`: desk
|
||||
walnut, metal, paper, carbon, plastic), downscaled to 512 px and credited on
|
||||
`/credits`. The social SVGs are used as monochrome CSS masks (`.social-mask`);
|
||||
`linkedin.svg` is a filled badge that masks to a plain square, so `web-ui.tsx`
|
||||
renders an inline mark for it instead. The desktop still uses the image assets.
|
||||
|
|
@ -235,7 +236,12 @@ not free placement. Don't confuse the two.
|
|||
collided with Tailwind's `w-screen` (`width: 100vw`) and silently stretched
|
||||
every panel to the viewport. Also remember `web.css` is unlayered, so its
|
||||
properties beat Tailwind utilities on the same element; use a dedicated class
|
||||
(e.g. `.w-navlink`) or `!` when a utility must win.
|
||||
(e.g. `.w-navlink`, `.w-corner-tr`) or `!` when a utility must win. (`.w-screw`
|
||||
sets `position: relative` for its slot, which is why screws are placed with
|
||||
`.w-corner-*` instead of Tailwind's `absolute`.)
|
||||
- Angles and offsets that hover states need to override live in CSS custom
|
||||
properties (e.g. `--tilt` on `.w-drive`), never in inline `style` transforms —
|
||||
an inline transform wins over `:hover`.
|
||||
- The desk reveals blocks with `.reveal` (see `Reveal` in `web-ui.tsx`): hidden by
|
||||
default, `.is-in` shows. On load the first screenful cascades top-to-bottom
|
||||
(90ms per block, in DOM order); the rest reveal when they scroll in. **Wrap
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dvorakmortonxd",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev-server.mjs",
|
||||
|
|
|
|||
BIN
public/textures/desk.jpg
Normal file
BIN
public/textures/desk.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
|
|
@ -1,13 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { useTheme } from "./useTheme";
|
||||
import { codingProjects, friends, socials, techProjects } from "../data";
|
||||
import {
|
||||
Bay,
|
||||
ContributionsPanel,
|
||||
Keyboard,
|
||||
Led,
|
||||
MiniRack,
|
||||
Monitor,
|
||||
Plaque,
|
||||
Report,
|
||||
Reveal,
|
||||
SectionHead,
|
||||
ServiceUnit,
|
||||
SocialGlyph,
|
||||
type ServiceAccess,
|
||||
|
|
@ -17,10 +21,10 @@ import {
|
|||
|
||||
const NAV = [
|
||||
{ href: "#about", label: "about" },
|
||||
{ href: "#services", label: "rack" },
|
||||
{ href: "#services", label: "services" },
|
||||
{ href: "#projects", label: "projects" },
|
||||
{ href: "#hardware", label: "inventory" },
|
||||
{ href: "#contact", label: "ports" },
|
||||
{ href: "#hardware", label: "machines" },
|
||||
{ href: "#contact", label: "contact" },
|
||||
];
|
||||
|
||||
const SERVICES: {
|
||||
|
|
@ -40,20 +44,9 @@ const SERVICES: {
|
|||
];
|
||||
|
||||
const LIFTS = [
|
||||
{ lift: "squat", weight: "125", unit: "kg", date: "05 Jan 2025" },
|
||||
{ lift: "bench", weight: "111", unit: "kg", date: "29 May 2026" },
|
||||
{ lift: "deadlift", weight: "165", unit: "kg", date: "17 Dec 2024" },
|
||||
];
|
||||
|
||||
const NEOFETCH: [string, string][] = [
|
||||
["host", "Proxmox VE · home rack"],
|
||||
["os", "Gentoo Linux (x86_64)"],
|
||||
["cpu", "Xeon E5-2680 v4 · 14c/28t"],
|
||||
["gpu", "RTX 3070 · passthrough"],
|
||||
["memory", "68 GB DDR4 RDIMM"],
|
||||
["storage", "256 GB NVMe · 1 TB · 750 GB"],
|
||||
["services", "6 containers, 0 subscriptions"],
|
||||
["uptime", "17 years"],
|
||||
{ lift: "squat", weight: "125", date: "05 Jan 2025" },
|
||||
{ lift: "bench", weight: "111", date: "29 May 2026" },
|
||||
{ lift: "deadlift", weight: "165", date: "17 Dec 2024" },
|
||||
];
|
||||
|
||||
/** Titles carry their state in parentheses, e.g. "dxdwm (Discontinued)". */
|
||||
|
|
@ -62,12 +55,12 @@ function splitStatus(title: string) {
|
|||
return m ? { name: m[1], status: m[2] } : { name: title, status: null };
|
||||
}
|
||||
|
||||
function statusKind(status: string | null): "green" | "amber" | "red" | "off" {
|
||||
function statusKind(status: string | null): "on" | "amber" | "off" {
|
||||
if (!status) return "off";
|
||||
const s = status.toLowerCase();
|
||||
if (s.includes("finished") || s.includes("available")) return "green";
|
||||
if (s.includes("finished") || s.includes("available")) return "on";
|
||||
if (s.includes("wip") || s.includes("unfinished")) return "amber";
|
||||
return "red";
|
||||
return "off";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ page */
|
||||
|
|
@ -76,30 +69,19 @@ export default function WebSite() {
|
|||
const { setTheme } = useTheme();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const stats = [
|
||||
{ value: String(SERVICES.length), label: "self-hosted services" },
|
||||
{ value: String(codingProjects.length), label: "projects on the shelf" },
|
||||
{ value: String(techProjects.length), label: "machines logged" },
|
||||
{ value: "401", label: "kg best SBD total" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-root">
|
||||
{/* ----------------------------------------------------------- rail */}
|
||||
<header className="w-rail w-metal-flat" style={{ borderRadius: 0 }}>
|
||||
<div className="shell flex h-16 items-center gap-5">
|
||||
<a href="#top" className="flex shrink-0 items-center gap-3" aria-label="Back to top">
|
||||
<Led kind="green" pulse />
|
||||
<span className="w-rack__title">dvorakmortonxd.local</span>
|
||||
<div className="w-page">
|
||||
{/* -------------------------------------------------------------- rail */}
|
||||
<header className="w-rail w-mt-metal-flush" style={{ borderRadius: 0 }}>
|
||||
<div className="mx-auto flex h-16 w-full max-w-[1120px] items-center gap-5 px-5 sm:px-8">
|
||||
<a href="#top" className="flex shrink-0 items-center gap-2.5" aria-label="Back to top">
|
||||
<Led kind="on" pulse slow />
|
||||
<span className="w-wordmark w-engrave">dvorakmortonxd</span>
|
||||
</a>
|
||||
|
||||
<nav className="ml-auto hidden items-center gap-6 md:flex" aria-label="Main">
|
||||
{NAV.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="w-navlink"
|
||||
>
|
||||
<a key={item.href} href={item.href} className="w-navlink">
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
|
@ -109,15 +91,16 @@ export default function WebSite() {
|
|||
<button
|
||||
type="button"
|
||||
aria-label="Switch to Desktop mode"
|
||||
data-desktop-mode="off"
|
||||
onClick={() => setTheme("skeuo")}
|
||||
className="desktop-toggle flex items-center gap-3"
|
||||
title="Flip to Desktop mode"
|
||||
>
|
||||
<span className="w-plate">desktop</span>
|
||||
<span className="w-switch">
|
||||
<span className="w-switch__knob" />
|
||||
<Led kind="off" className="w-switch__led" />
|
||||
<span className="w-mono hidden text-[0.6rem] tracking-[0.2em] text-[var(--ftext-faint)] sm:inline">
|
||||
DESKTOP
|
||||
</span>
|
||||
<span className="w-rocker">
|
||||
<span className="w-rocker__knob" />
|
||||
<Led kind="off" className="w-rocker__led" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -127,7 +110,7 @@ export default function WebSite() {
|
|||
onClick={() => setMenuOpen((v) => !v)}
|
||||
aria-expanded={menuOpen}
|
||||
aria-label={menuOpen ? "Close menu" : "Open menu"}
|
||||
className="w-btn px-3 py-2"
|
||||
className="w-keybtn !px-3 !py-2"
|
||||
>
|
||||
{menuOpen ? "close" : "menu"}
|
||||
</button>
|
||||
|
|
@ -136,13 +119,16 @@ export default function WebSite() {
|
|||
</div>
|
||||
|
||||
{menuOpen ? (
|
||||
<nav className="shell w-metal-flat mb-3 mt-1 flex flex-col gap-1 p-3 md:hidden" aria-label="Main">
|
||||
<nav
|
||||
className="w-mt-metal-flush mx-auto mb-3 flex w-[calc(100%-2.5rem)] max-w-[1120px] flex-col gap-1 rounded-lg p-3 md:hidden"
|
||||
aria-label="Main"
|
||||
>
|
||||
{NAV.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="w-unit__name block border-b border-[rgba(255,255,255,0.06)] py-3"
|
||||
className="w-mono border-b border-[rgba(255,226,180,0.07)] py-3 text-[0.78rem] text-[var(--ftext-dim)] last:border-b-0"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
|
|
@ -153,177 +139,205 @@ export default function WebSite() {
|
|||
|
||||
<span id="top" />
|
||||
|
||||
<main className="pt-16">
|
||||
{/* --------------------------------------------------------- hero */}
|
||||
<section className="shell pt-12 pb-14 sm:pt-16">
|
||||
<main className="pt-24 sm:pt-28">
|
||||
{/* ------------------------------------------------------------ hero */}
|
||||
<section className="mx-auto w-full max-w-[1120px] px-5 sm:px-8">
|
||||
<div className="grid gap-10 lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)] lg:items-end lg:gap-8">
|
||||
<Reveal>
|
||||
<div className="w-rack w-metal">
|
||||
<div className="w-rack__head">
|
||||
<Led kind="green" pulse />
|
||||
<span className="w-rack__title">dvorakmortonxd.local</span>
|
||||
<span className="w-unit__meta ml-auto hidden sm:block">4U · asus p9x79 · 24/7</span>
|
||||
<span className="w-screw ml-2 hidden sm:block" />
|
||||
<span className="w-screw hidden sm:block" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)]">
|
||||
<div className="w-nameplate min-w-0">
|
||||
<p className="w-kicker">homelab · czech republic · online</p>
|
||||
<h1 className="mt-4 text-[2rem] leading-[1.05] sm:text-[2.6rem] lg:text-[3rem]">
|
||||
<Monitor>
|
||||
<p className="w-tft">
|
||||
<span className="a">martin@dvorakmortonxd</span>:~$ whoami
|
||||
<span className="w-caret ml-2" />
|
||||
</p>
|
||||
<h1 className="w-serif mt-5 text-[clamp(1.85rem,4.2vw,2.85rem)] text-[var(--ftext)]">
|
||||
My own little internet — one rack, zero subscriptions.
|
||||
</h1>
|
||||
<p className="w-prose mt-5 max-w-[48ch]">
|
||||
I’m Martin Dvořák. Git, media, music, an AI box and a Minecraft server all run on a
|
||||
<p className="w-prose mt-5 max-w-[54ch] text-[var(--ftext-dim)]">
|
||||
I’m Martin Dvořák. Git, media, music, an AI box and a Minecraft server all run on a
|
||||
Proxmox machine in my room — plus the hardware graveyard I keep rescuing from e-waste.
|
||||
</p>
|
||||
<div className="mt-7 flex flex-wrap items-center gap-3">
|
||||
<a href="#services" className="w-btn w-btn--accent">
|
||||
<Led kind="amber" /> see what I run
|
||||
<p className="mt-7 flex flex-wrap gap-x-8 gap-y-3">
|
||||
<a href="#services" className="w-cmd">
|
||||
$ open rack --services
|
||||
</a>
|
||||
<a href="#projects" className="w-btn">
|
||||
open the drive drawer
|
||||
<a href="#projects" className="w-cmd">
|
||||
$ open drawer --projects
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-panel min-w-0 p-4">
|
||||
<div className="w-rack__head !border-b-0 !pb-2">
|
||||
<span className="w-rack__title">console — neofetch</span>
|
||||
<Led kind="green" className="ml-auto" />
|
||||
</div>
|
||||
<div className="w-screen-text">
|
||||
{NEOFETCH.map(([k, v]) => (
|
||||
<p key={k} className="flex gap-3">
|
||||
<span className="k w-[8ch] shrink-0">{k}</span>
|
||||
<span className="v min-w-0 truncate">{v}</span>
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-1">
|
||||
<span className="k">$</span> <span className="w-cursor" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Monitor>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((s) => (
|
||||
<div key={s.label} className="w-unit w-metal-flat">
|
||||
<Led kind="amber" />
|
||||
<span className="min-w-0">
|
||||
<span className="w-digits block text-[1.5rem] leading-none">{s.value}</span>
|
||||
<span className="w-unit__meta mt-1 block truncate">{s.label}</span>
|
||||
</span>
|
||||
<div className="-mt-1 px-1 pt-7 sm:px-8">
|
||||
<Keyboard />
|
||||
</div>
|
||||
))}
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={120}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<MiniRack />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Plaque value={String(SERVICES.length)} label="services" />
|
||||
<Plaque value={String(codingProjects.length)} label="projects" />
|
||||
<Plaque value={String(techProjects.length)} label="machines" />
|
||||
<Plaque value="401" label="kg best SBD" />
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shell flex flex-col gap-16 pb-20 sm:gap-24">
|
||||
{/* -------------------------------------------------------- about */}
|
||||
<Bay id="about" index="01" label="about" title="Who is behind the rack">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Reveal delay={40}>
|
||||
<article className="w-paper w-card h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-grommet" />
|
||||
<span className="w-kicker !text-[var(--paper-ink-soft)]">manifest.txt</span>
|
||||
<div className="mx-auto flex w-full max-w-[1120px] flex-col gap-24 px-5 pt-28 pb-24 sm:gap-28 sm:px-8">
|
||||
{/* --------------------------------------------------------- about */}
|
||||
<section id="about" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="01" label="about" title="Who is behind the rack" />
|
||||
</Reveal>
|
||||
<div className="grid gap-7 lg:grid-cols-[minmax(0,1.55fr)_minmax(0,1fr)] lg:items-start">
|
||||
<Reveal delay={60}>
|
||||
<article className="w-mt-paper w-sheet">
|
||||
<div className="w-sheet__head">
|
||||
<span>manifest.txt</span>
|
||||
<span className="ml-auto">printed on the desk</span>
|
||||
</div>
|
||||
<p className="w-prose mt-4">
|
||||
I’m a sophomore at{" "}
|
||||
<div className="mt-5 grid gap-x-9 gap-y-4 sm:grid-cols-2">
|
||||
<p className="w-prose">
|
||||
I’m a sophomore at{" "}
|
||||
<a className="w-link" href="https://sssvt.cz" target="_blank" rel="noopener noreferrer">
|
||||
sssvt.cz
|
||||
</a>
|
||||
, studying network management and administration. I wanted to go there since 7th
|
||||
grade, so getting in was a good day.
|
||||
</p>
|
||||
<p className="w-prose mt-3">
|
||||
I’m a homelabber first: my own Git, media, music, AI and game servers, most of it
|
||||
<p className="w-prose">
|
||||
I’m a homelabber first: my own Git, media, music, AI and game servers, most of it
|
||||
running on hardware other people threw away.
|
||||
</p>
|
||||
</article>
|
||||
</Reveal>
|
||||
<Reveal delay={130}>
|
||||
<article className="w-paper w-card w-note--tilt h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-grommet" />
|
||||
<span className="w-kicker !text-[var(--paper-ink-soft)]">inventory.txt</span>
|
||||
</div>
|
||||
<p className="w-prose mt-4">
|
||||
<p className="w-prose">
|
||||
A laptop collection from 2009 to 2025, mostly e-waste rescues — ThinkPads, MacBooks,
|
||||
a Legion Go, an Xbox with a bad-update exploit and one very dead 2010 unibody.
|
||||
</p>
|
||||
<p className="w-prose mt-3">
|
||||
<p className="w-prose">
|
||||
Away from the rack: PPL training 3–5× a week, Terraria (~1300 h), Minecraft modpacks
|
||||
and the occasional game jam.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Bay>
|
||||
|
||||
{/* ---------------------------------------------------------- now */}
|
||||
<Bay id="now" index="02" label="now" title="On the bench right now">
|
||||
<Reveal delay={40}>
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)]">
|
||||
<article className="w-paper w-note min-w-0">
|
||||
<span className="w-kicker !text-[var(--paper-ink-soft)]">now.txt · updated 09/2026</span>
|
||||
<p className="w-prose mt-4">
|
||||
I’m hosting an SMP — message me on Discord if you want in. I’m also cooking most days,
|
||||
bulking through the non-summer seasons.
|
||||
<Reveal delay={150}>
|
||||
<aside className="w-mt-paper w-cardindex lg:mt-12">
|
||||
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[var(--ink-soft)]">
|
||||
INDEX CARD
|
||||
</p>
|
||||
<p className="w-prose mt-3">
|
||||
<p className="w-serif mt-3 text-[1.15rem] text-[var(--ink)]">
|
||||
“If it has a fan, I’ll find a use for it.”
|
||||
</p>
|
||||
<div className="w-rule my-4" />
|
||||
<ul className="w-mono space-y-2 text-[0.66rem] text-[var(--ink-soft)]">
|
||||
<li>school · sssvt.cz</li>
|
||||
<li>gym · PPL 3–5×/week</li>
|
||||
<li>games · Terraria, Minecraft</li>
|
||||
<li>currently · bulking season</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ----------------------------------------------------------- now */}
|
||||
<section id="now" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="02" label="now" title="On the bench right now" />
|
||||
</Reveal>
|
||||
<div className="grid gap-7 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)] lg:items-start">
|
||||
<Reveal delay={60}>
|
||||
<div className="w-sticky">
|
||||
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[#6b6047]">
|
||||
NOW.TXT · UPDATED 09/2026
|
||||
</p>
|
||||
<p className="w-prose mt-4 text-[#4a4232]">
|
||||
I’m hosting an SMP — message me on Discord if you want in. I’m also cooking most
|
||||
days, bulking through the non-summer seasons.
|
||||
</p>
|
||||
<p className="w-prose mt-3 text-[#4a4232]">
|
||||
Me and my friend{" "}
|
||||
<a className="w-link" href="https://skopec.dev" target="_blank" rel="noopener noreferrer">
|
||||
Pepa
|
||||
</a>{" "}
|
||||
are building an education app so we can study more efficiently. It’s called{" "}
|
||||
<a className="w-link" href="https://git.dvorakmortonxd.dev/edu/maturita.c" target="_blank" rel="noopener noreferrer">
|
||||
are building an education app so we can study more efficiently. It’s called{" "}
|
||||
<a
|
||||
className="w-link"
|
||||
href="https://git.dvorakmortonxd.dev/edu/maturita.c"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
maturita.c
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<div className="w-panel min-w-0 p-4">
|
||||
<div className="w-rack__head !border-b-0 !pb-2">
|
||||
<span className="w-rack__title">status</span>
|
||||
<Led kind="green" pulse className="ml-auto" />
|
||||
</div>
|
||||
<div className="w-screen-text">
|
||||
<p><span className="k">smp</span> <span className="v">accepting players</span></p>
|
||||
<p><span className="k">bulk</span> <span className="v">in progress</span></p>
|
||||
<p><span className="k">maturita.c</span> <span className="v">in development</span></p>
|
||||
<p><span className="k">gym</span> <span className="v">PPL · 3–5×/week</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
|
||||
{/* ----------------------------------------------------- services */}
|
||||
<Bay id="services" index="03" label="rack" title="Rack elevation — live status">
|
||||
<Reveal delay={40}>
|
||||
<div className="w-rack w-metal">
|
||||
<div className="w-rack__head">
|
||||
<span className="w-rack__title">services</span>
|
||||
<span className="w-unit__meta ml-auto">checked through this site</span>
|
||||
<Reveal delay={140}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="w-mt-paper w-cardindex w-cardindex--alt">
|
||||
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[var(--ink-soft)]">
|
||||
BENCH STATUS
|
||||
</p>
|
||||
<ul className="w-mono mt-3 space-y-2 text-[0.7rem] text-[var(--ink)]">
|
||||
<li className="flex items-center gap-2">
|
||||
<Led kind="on" /> smp · accepting players
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Led kind="amber" pulse /> maturita.c · in development
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Led kind="on" /> gym · PPL 3–5×/week
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<i className="w-pencil ml-6 w-44" aria-hidden="true" />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ------------------------------------------------------ services */}
|
||||
<section id="services" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="03" label="rack" title="Six machines, one shelf" />
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="w-mt-metal w-rack">
|
||||
<div className="w-rack__top">
|
||||
<Led kind="on" pulse slow />
|
||||
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">RACK 01 · SERVICES</span>
|
||||
<span className="w-mono ml-auto hidden text-[0.6rem] tracking-[0.14em] text-[var(--ftext-faint)] sm:inline">
|
||||
health checked from this page
|
||||
</span>
|
||||
<span className="w-rack__vent w-vents" />
|
||||
</div>
|
||||
<div className="w-rack__grid">
|
||||
<span className="w-rails" />
|
||||
<div>
|
||||
{SERVICES.map((s) => (
|
||||
<ServiceUnit key={s.name} {...s} />
|
||||
))}
|
||||
</div>
|
||||
<span className="w-rails" />
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
</section>
|
||||
|
||||
{/* ----------------------------------------------------- projects */}
|
||||
<Bay id="projects" index="04" label="projects" title="The drive drawer">
|
||||
<Reveal delay={40}>
|
||||
<div className="w-drawer w-metal">
|
||||
<div className="w-drawer__handle mx-auto mb-4 w-40" />
|
||||
<div className="max-h-[560px] overflow-y-auto pr-1">
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{/* ------------------------------------------------------ projects */}
|
||||
<section id="projects" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="04" label="drawer" title="Eighteen drives, all labelled" />
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="w-mt-metal w-drawer">
|
||||
<div className="w-mt-plastic w-drawer__inner w-scroll">
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{codingProjects.map((p, i) => {
|
||||
const { name, status } = splitStatus(p.title);
|
||||
return (
|
||||
|
|
@ -332,131 +346,164 @@ export default function WebSite() {
|
|||
href={p.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-caddy w-carbon"
|
||||
className="w-drive"
|
||||
style={{ "--tilt": `${i % 2 ? 0.5 : -0.6}deg` } as CSSProperties}
|
||||
title={p.desc}
|
||||
>
|
||||
<Led kind={statusKind(status)} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="w-caddy__label block truncate">{name}</span>
|
||||
<span className="w-caddy__status block truncate">
|
||||
<span className="w-drive__port" aria-hidden="true" />
|
||||
<span className="w-drive__sticker">
|
||||
<span className="w-drive__name">{name}</span>
|
||||
<span className="w-drive__meta">
|
||||
{status ?? "unlabelled"} · {p.desc}
|
||||
</span>
|
||||
</span>
|
||||
<Led kind={statusKind(status)} className="w-drive__led" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-mt-metal-flush w-drawer__front">
|
||||
<i className="w-screw" aria-hidden="true" />
|
||||
<span className="w-drawer__handle" aria-hidden="true" />
|
||||
<i className="w-screw" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
</section>
|
||||
|
||||
{/* ----------------------------------------------------- hardware */}
|
||||
<Bay id="hardware" index="05" label="inventory" title="On the pegboard">
|
||||
<Reveal delay={40}>
|
||||
<div className="w-pegboard w-metal">
|
||||
{/* ------------------------------------------------------ hardware */}
|
||||
<section id="hardware" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="05" label="machines" title="Tagged on the pegboard" />
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="w-mt-metal w-pegboard">
|
||||
<div className="w-pegboard__face">
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{techProjects.map((t) => (
|
||||
<article key={t.title} className="w-paper w-tag">
|
||||
<span className="w-tag__ring" />
|
||||
<h3 className="w-serif text-[1rem]">{t.title}</h3>
|
||||
<p className="w-prose mt-2 text-[0.8rem]">{t.desc}</p>
|
||||
<article key={t.title} className="w-mt-paper w-hangtag">
|
||||
<span className="w-hangtag__hole" aria-hidden="true" />
|
||||
<h3 className="w-serif text-[1.02rem] text-[var(--ink)]">{t.title}</h3>
|
||||
<p className="w-prose mt-2 text-[0.8rem] text-[var(--ink-soft)]">{t.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
</section>
|
||||
|
||||
{/* -------------------------------------------------- contributions */}
|
||||
<Bay id="gitdxd" index="06" label="gitdxd" title="Forge telemetry">
|
||||
<Reveal delay={40}>
|
||||
<ContributionsPanel />
|
||||
{/* -------------------------------------------------- activity feed */}
|
||||
<section id="gitdxd" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="06" label="gitdxd" title="The printer spits out the forge" />
|
||||
</Reveal>
|
||||
</Bay>
|
||||
<Reveal delay={60}>
|
||||
<Report />
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* -------------------------------------------------------- friends */}
|
||||
<Bay id="friends" index="07" label="friends" title="Cards on the desk">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{/* ------------------------------------------------------- friends */}
|
||||
<section id="friends" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="07" label="friends" title="Cards on the desk" />
|
||||
</Reveal>
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{friends.map((f, i) => (
|
||||
<Reveal key={f.name} delay={40 + i * 70}>
|
||||
<Reveal key={f.name} delay={60 + i * 70}>
|
||||
<a
|
||||
href={f.realUrl || f.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`w-paper w-card block transition-transform hover:-translate-y-1 ${i % 2 ? "w-note--tilt" : ""}`}
|
||||
className="w-mt-paper w-bizcard"
|
||||
style={{ "--tilt": `${i === 1 ? 0.9 : i === 2 ? -1 : -1.4}deg` } as CSSProperties}
|
||||
>
|
||||
<h3 className="w-serif text-[1.1rem]">{f.name}</h3>
|
||||
<p className="mt-1 font-mono text-[0.72rem] text-[var(--paper-ink-soft)]">{f.desc}</p>
|
||||
<p className="w-serif text-[1.15rem] text-[var(--ink)]">{f.name}</p>
|
||||
<p className="w-mono mt-1 text-[0.66rem] text-[var(--ink-soft)]">{f.desc}</p>
|
||||
</a>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</Bay>
|
||||
</section>
|
||||
|
||||
{/* ------------------------------------------------------------ gym */}
|
||||
<Bay id="gym" index="08" label="strength" title="On the platform">
|
||||
<Reveal delay={40}>
|
||||
<div className="w-metal p-5">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{LIFTS.map((l) => (
|
||||
<div key={l.lift} className="w-panel p-4 text-center">
|
||||
<div className="w-digits text-[2.2rem] leading-none">
|
||||
{l.weight}
|
||||
<span className="ml-1 text-[1rem] opacity-70">{l.unit}</span>
|
||||
{/* ----------------------------------------------------------- gym */}
|
||||
<section id="gym" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="08" label="strength" title="Numbers on the clipboard" />
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="w-clipboard">
|
||||
<div className="w-mt-metal w-clipboard__clip" aria-hidden="true" />
|
||||
<div className="w-mt-paper w-logsheet">
|
||||
<div className="w-sheet__head">
|
||||
<span>training log</span>
|
||||
<span className="ml-auto">73 kg bodyweight</span>
|
||||
</div>
|
||||
<div className="w-kicker mt-3">{l.lift}</div>
|
||||
<div className="w-unit__meta mt-1">{l.date} · 73 kg BW</div>
|
||||
<div className="mt-3">
|
||||
{LIFTS.map((l) => (
|
||||
<div key={l.lift} className="w-logrow">
|
||||
<span className="w-logrow__lift">{l.lift}</span>
|
||||
<span className="w-logrow__weight">{l.weight} kg</span>
|
||||
<span className="w-logrow__date">{l.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="w-mono mt-4 text-[0.62rem] tracking-[0.14em] text-[var(--ink-soft)]">
|
||||
BEST SBD TOTAL · 401 KG · 165 + 111 + 125
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
</section>
|
||||
|
||||
{/* -------------------------------------------------------- contact */}
|
||||
<Bay id="contact" index="09" label="ports" title="Patch panel">
|
||||
<Reveal delay={40}>
|
||||
<div className="w-patch w-metal">
|
||||
<div className="w-rack__head">
|
||||
<span className="w-rack__title">socials</span>
|
||||
<span className="w-unit__meta ml-auto">12 ports</span>
|
||||
{/* ------------------------------------------------------- contact */}
|
||||
<section id="contact" className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<SectionHead index="09" label="patch panel" title="Twelve ports, all live" />
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="w-mt-metal w-patch">
|
||||
<div className="w-mt-metal-flush w-patch__face">
|
||||
<div className="w-patch__head">
|
||||
<i className="w-screw" aria-hidden="true" />
|
||||
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">PATCH 01 · SOCIALS</span>
|
||||
<i className="w-screw ml-auto" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid gap-2.5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{socials.map((s) => (
|
||||
<a key={s.name} href={s.url} target="_blank" rel="noopener noreferrer" className="w-port">
|
||||
<span className="w-port__jack" />
|
||||
<span className="text-[var(--faint)]">
|
||||
<span className="w-port__jack" aria-hidden="true">
|
||||
<span className="w-port__slot" />
|
||||
</span>
|
||||
<span className="w-port__icon">
|
||||
<SocialGlyph name={s.name} icon={s.icon} />
|
||||
</span>
|
||||
<span className="w-port__label min-w-0 truncate">{s.name}</span>
|
||||
<Led kind="green" className="ml-auto" />
|
||||
<span className="w-port__label">{s.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<p className="w-unit__meta mt-5">
|
||||
<p className="w-mono mt-5 text-[0.64rem] tracking-[0.06em] text-[var(--ftext-faint)]">
|
||||
fastest reply is Discord or Instagram · mail{" "}
|
||||
<a className="w-link" href="mailto:dvorak@dvorakmortonxd.dev">
|
||||
dvorak@dvorakmortonxd.dev
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</Bay>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* --------------------------------------------------------- footer */}
|
||||
<footer className="shell pb-14 pt-10">
|
||||
{/* ------------------------------------------------------------ footer */}
|
||||
<footer className="mx-auto w-full max-w-[1120px] px-5 pb-16 sm:px-8">
|
||||
<Reveal>
|
||||
<div className="w-metal-flat flex flex-col gap-5 p-5 md:flex-row md:items-center md:justify-between">
|
||||
<div className="w-mt-brass relative flex flex-col gap-5 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<Led kind="green" />
|
||||
<span className="w-rack__title">dvorakmortonxd.local</span>
|
||||
</div>
|
||||
<p className="w-unit__meta mt-2">
|
||||
built with Next.js · textures CC0 via ambientCG ·{" "}
|
||||
<p className="w-mono text-[0.72rem] tracking-[0.2em]">DVORAKMORTONXD.LOCAL</p>
|
||||
<p className="w-mono mt-1.5 text-[0.62rem] opacity-80">
|
||||
v3.2.0 · GPL-3.0-only · built with Next.js · textures CC0 via{" "}
|
||||
<a className="w-link" href="/credits">
|
||||
credits
|
||||
</a>
|
||||
|
|
@ -465,17 +512,17 @@ export default function WebSite() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme("skeuo")}
|
||||
className="w-btn desktop-toggle shrink-0"
|
||||
className="w-keybtn desktop-toggle shrink-0 self-start sm:self-auto"
|
||||
title="Flip to Desktop mode"
|
||||
>
|
||||
<Led kind="amber" /> desktop mode
|
||||
</button>
|
||||
<i className="w-screw w-screw--brass w-corner-tr" aria-hidden="true" />
|
||||
<i className="w-screw w-screw--brass w-corner-bl" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-1 font-mono text-[0.68rem] text-[var(--faint)] sm:flex-row sm:justify-between">
|
||||
<span>v3.1.0 · © 2025–2026 Martin Dvořák</span>
|
||||
<span>GPL-3.0-only — follow it or don’t, I don’t mind</span>
|
||||
</div>
|
||||
<p className="w-mono mt-4 text-[0.62rem] text-[var(--ftext-faint)]">
|
||||
© 2025–2026 Martin Dvořák — follow the licence or don’t, I don’t mind
|
||||
</p>
|
||||
</Reveal>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function Reveal({
|
|||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -12% 0px", threshold: 0.08 }
|
||||
{ rootMargin: "0px 0px -12% 0px", threshold: 0.06 }
|
||||
);
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
|
@ -58,51 +58,132 @@ export function Reveal({
|
|||
/* ------------------------------------------------------------- hardware */
|
||||
|
||||
export function Led({
|
||||
kind = "green",
|
||||
kind = "on",
|
||||
pulse = false,
|
||||
slow = false,
|
||||
className = "",
|
||||
title,
|
||||
style,
|
||||
}: {
|
||||
kind?: "green" | "amber" | "red" | "blue" | "off";
|
||||
kind?: "on" | "amber" | "off";
|
||||
pulse?: boolean;
|
||||
slow?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
style?: CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={`w-led w-led--${kind}${pulse ? " w-led--pulse" : ""} ${className}`}
|
||||
title={title}
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className={`w-led w-led--${kind}${pulse ? (slow ? " w-led--pulse-slow" : " w-led--pulse") : ""} ${className}`}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Section header, mounted on the chassis. */
|
||||
export function Bay({
|
||||
id,
|
||||
index,
|
||||
label,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
index: string;
|
||||
label: string;
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
export function Screws({ count = 2, brass = false, className = "" }: { count?: number; brass?: boolean; className?: string }) {
|
||||
return (
|
||||
<section id={id} className="scroll-mt-28">
|
||||
<Reveal>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-3">
|
||||
<span className="w-plate">
|
||||
{index} <span className="opacity-50">/</span> {label}
|
||||
<span className={`w-unit__screws ${className}`}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<i key={i} className={`w-screw${brass ? " w-screw--brass" : ""}`} />
|
||||
))}
|
||||
</span>
|
||||
<h2 className="w-serif text-[1.6rem] sm:text-[2rem]">{title}</h2>
|
||||
);
|
||||
}
|
||||
|
||||
/** Section heading: a brass tab screwed to the desk + painted title. */
|
||||
export function SectionHead({ index, label, title }: { index: string; label: string; title: string }) {
|
||||
return (
|
||||
<div className="w-sechead">
|
||||
<span className="w-mt-brass w-sechead__tab">{index} · {label}</span>
|
||||
<h2 className="w-serif w-paint w-sechead__title">{title}</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- the machine */
|
||||
|
||||
export function Monitor({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={`w-monitor ${className}`}>
|
||||
<div className="w-monitor__frame">
|
||||
<div className="w-mt-glass w-monitor__glass">{children}</div>
|
||||
<div className="w-monitor__chin">
|
||||
<Led kind="amber" />
|
||||
<span className="w-monitor__brand">DXD</span>
|
||||
<span className="w-monitor__osd" aria-hidden="true">
|
||||
<i className="w-osdkey" />
|
||||
<i className="w-osdkey" />
|
||||
<i className="w-osdkey" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-monitor__stand" aria-hidden="true">
|
||||
<i className="w-monitor__neck" />
|
||||
<i className="w-monitor__base" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const KEY_ROWS = ["1234567890-=".split(""), "qwertyuiop[]".split(""), "asdfghjkl;'".split("")];
|
||||
|
||||
export function Keyboard() {
|
||||
return (
|
||||
<div className="w-keyboard" aria-hidden="true">
|
||||
<div className="w-keyboard__deck">
|
||||
{KEY_ROWS.map((row, i) => (
|
||||
<div key={i} className="w-keyboard__row">
|
||||
{row.map((k) => (
|
||||
<span key={k} className="w-key">
|
||||
{k}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-keyboard__row">
|
||||
<span className="w-key w-key--space" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A small rack that sits on the desk in the hero. */
|
||||
export function MiniRack({ label = "DXD-01" }: { label?: string }) {
|
||||
return (
|
||||
<div className="w-rack w-mt-metal" aria-hidden="true">
|
||||
<div className="w-rack__top">
|
||||
<Led kind="on" pulse />
|
||||
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">{label}</span>
|
||||
<span className="w-rack__vent w-vents" />
|
||||
</div>
|
||||
<div className="w-rack__grid">
|
||||
<span className="w-rails" />
|
||||
<div>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div key={i} className="w-unit !min-h-[46px] !gap-[10px] !py-2">
|
||||
<span className="w-unit__grille !h-6 !w-9" />
|
||||
<Led kind="on" pulse style={{ animationDelay: `${i * 0.5}s` }} />
|
||||
<Led kind="amber" pulse slow style={{ animationDelay: `${i * 0.3}s` }} />
|
||||
<span className="ml-auto w-engrave text-[0.58rem] tracking-[0.14em]">
|
||||
{["pxe", "aio", "nas", "web"][i]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="w-rails" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A brass plate with one number stamped into it. */
|
||||
export function Plaque({ value, label }: { value: string; label: string }) {
|
||||
return (
|
||||
<div className="w-mt-brass w-plaque relative">
|
||||
<span className="w-plaque__value">{value}</span>
|
||||
<span className="w-plaque__label">{label}</span>
|
||||
<i className="w-screw w-screw--brass w-plaque__screw" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<div className="mt-8">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -143,17 +224,10 @@ export function ServiceUnit({
|
|||
};
|
||||
}, [url, checkUrl, disabled]);
|
||||
|
||||
const effective = disabled ? "up" : status;
|
||||
const kind = effective === "up" ? "green" : effective === "down" ? "red" : "off";
|
||||
const statusLabel = disabled
|
||||
? "standby"
|
||||
: effective === "up"
|
||||
? "online"
|
||||
: effective === "down"
|
||||
? "offline"
|
||||
: "checking";
|
||||
const accessLabel =
|
||||
access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
|
||||
const live = disabled ? "up" : status;
|
||||
const kind = live === "up" ? "on" : live === "down" ? "off" : "amber";
|
||||
const label = disabled ? "STANDBY" : live === "up" ? "ONLINE" : live === "down" ? "OFFLINE" : "POLLING";
|
||||
const accessLabel = access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
|
||||
|
||||
const host = (() => {
|
||||
try {
|
||||
|
|
@ -165,42 +239,39 @@ export function ServiceUnit({
|
|||
|
||||
const inner = (
|
||||
<>
|
||||
<span className="w-unit__leds">
|
||||
<Led kind={kind} pulse={effective === "up"} />
|
||||
<Led kind="amber" pulse={effective === "up"} />
|
||||
<span className="w-unit__grip" aria-hidden="true" />
|
||||
<span className="w-unit__grille w-grille" aria-hidden="true" />
|
||||
<span className="w-unit__id">
|
||||
<span className="w-unit__name w-engrave">{name}</span>
|
||||
<span className="w-tape mt-1">
|
||||
{host}
|
||||
{accessLabel ? ` · ${accessLabel}` : ""}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="w-unit__name block truncate">{name}</span>
|
||||
<span className="w-unit__meta block truncate">
|
||||
{host} {accessLabel ? `· ${accessLabel}` : ""}
|
||||
<span className="sr-only">{desc}</span>
|
||||
</span>
|
||||
<span className="w-unit__state">
|
||||
<Led kind={kind} pulse={live === "up"} />
|
||||
<span className="w-unit__status w-engrave">{label}</span>
|
||||
</span>
|
||||
<span className="hidden text-right sm:block">
|
||||
<span className="w-unit__meta block max-w-[26ch] truncate">{desc}</span>
|
||||
</span>
|
||||
<span className="w-plate shrink-0">
|
||||
<Led kind={kind} /> {statusLabel}
|
||||
</span>
|
||||
<Screws />
|
||||
</>
|
||||
);
|
||||
|
||||
const cls = "w-unit w-metal-flat";
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={cls} aria-disabled="true">
|
||||
<span className="w-unit" aria-disabled="true">
|
||||
{inner}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className={`${cls} w-unit--link`}>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className="w-unit w-unit--link" title={desc}>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- contributions */
|
||||
/* -------------------------------------------------------- activity report */
|
||||
|
||||
interface HeatmapPoint {
|
||||
timestamp: number;
|
||||
|
|
@ -213,7 +284,9 @@ interface Activity {
|
|||
repo?: { full_name?: string };
|
||||
}
|
||||
|
||||
export function ContributionsPanel() {
|
||||
const DOT_OPACITY = [0.07, 0.3, 0.48, 0.64, 0.82];
|
||||
|
||||
export function Report() {
|
||||
const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [activities, setActivities] = useState<Activity[]>([]);
|
||||
|
|
@ -291,50 +364,52 @@ export function ContributionsPanel() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// level 4 = brightest LED, level 0 = off
|
||||
const levelOn = (l: number) => (l <= 0 ? "0" : String(5 - l));
|
||||
|
||||
return (
|
||||
<div className="w-dashboard w-metal">
|
||||
<div className="w-rack__head">
|
||||
<span className="w-rack__title">gitdxd.activity</span>
|
||||
<span className="ml-auto flex items-center gap-2">
|
||||
<Led kind={state === "error" ? "red" : "green"} pulse={state === "ready"} />
|
||||
<span className="w-unit__meta">{state === "error" ? "link down" : "streaming"}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-printout">
|
||||
<div className="w-mt-paper w-printout__paper">
|
||||
<span className="w-printout__perf w-printout__perf--l" aria-hidden="true" />
|
||||
<span className="w-printout__perf w-printout__perf--r" aria-hidden="true" />
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-end justify-between gap-3">
|
||||
<div className="w-digits text-[2rem] leading-none sm:text-[2.5rem]">{total}</div>
|
||||
<div className="w-kicker">contributions / last year</div>
|
||||
</div>
|
||||
<p className="w-print__head">
|
||||
<span>GITDXD // ACTIVITY REPORT</span>
|
||||
<span className="ml-auto">{state === "ready" ? "PRINTED LIVE" : state === "error" ? "PRINTER OFFLINE" : "SPOOLING…"}</span>
|
||||
</p>
|
||||
|
||||
{state === "loading" ? (
|
||||
<p className="w-screen-text mt-5 opacity-70">reading matrix…</p>
|
||||
) : state === "error" ? (
|
||||
<p className="w-screen-text mt-5 opacity-70">could not reach the forge.</p>
|
||||
{state === "error" ? (
|
||||
<p className="w-mono mt-5 text-[0.72rem] text-[#4a4133]">could not reach the forge.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-ledgrid mt-5">
|
||||
<div className="mt-5 flex flex-wrap items-end gap-x-6 gap-y-2">
|
||||
<div>
|
||||
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[#4a4133]">TOTAL / 12 MONTHS</p>
|
||||
<p className="w-serif text-[2.6rem] leading-none text-[var(--ink)]">
|
||||
{state === "loading" ? "—" : total}
|
||||
</p>
|
||||
</div>
|
||||
<p className="w-mono text-[0.62rem] leading-relaxed text-[#6f6450]">
|
||||
each dot = one day
|
||||
<br />
|
||||
darkest = busiest
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-dotgrid w-scroll" aria-hidden={state === "loading"}>
|
||||
{weeks.map((w, i) => (
|
||||
<div key={i} className="w-ledgrid__col">
|
||||
<span key={i} className="w-dotgrid__col">
|
||||
{w.map((d) => (
|
||||
<div key={d.date} className="w-ledgrid__cell" data-on={levelOn(d.level)} title={`${d.contribs} on ${d.date}`} />
|
||||
<span
|
||||
key={d.date}
|
||||
className="w-dot"
|
||||
style={{ opacity: DOT_OPACITY[d.level] }}
|
||||
title={`${d.contribs} on ${d.date}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-end gap-2">
|
||||
<span className="w-unit__meta">less</span>
|
||||
{[4, 3, 2, 1, 0].map((l) => (
|
||||
<span key={l} className="w-ledgrid__cell" data-on={levelOn(l)} />
|
||||
))}
|
||||
<span className="w-unit__meta">more</span>
|
||||
</div>
|
||||
|
||||
{activities.length > 0 ? (
|
||||
<div className="w-panel mt-5 p-3">
|
||||
<div className="w-screen-text">
|
||||
<div className="w-print__log">
|
||||
{activities.map((a, i) => {
|
||||
let msg = "commit";
|
||||
try {
|
||||
|
|
@ -353,16 +428,16 @@ export function ContributionsPanel() {
|
|||
}
|
||||
return (
|
||||
<p key={i} className="truncate">
|
||||
<span className="k">$</span> {a.repo?.full_name || "unknown"} — {msg}
|
||||
> {a.repo?.full_name || "unknown"} — {msg}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,57 +8,58 @@ export const metadata: Metadata = {
|
|||
};
|
||||
|
||||
const TEXTURES = [
|
||||
{ file: "metal.jpg", asset: "Metal 051 C", id: "Metal051C" },
|
||||
{ file: "paper.jpg", asset: "Paper 003", id: "Paper003" },
|
||||
{ file: "carbon.jpg", asset: "Fabric 004", id: "Fabric004" },
|
||||
{ file: "rubber.jpg", asset: "Rubber 004", id: "Rubber004" },
|
||||
{ file: "plastic.jpg", asset: "Plastic 012 A", id: "Plastic012A" },
|
||||
{ file: "desk.jpg", asset: "Wood 027", id: "Wood027", use: "the desk the page sits on" },
|
||||
{ file: "metal.jpg", asset: "Metal 051 C", id: "Metal051C", use: "rack, monitor, panel hardware" },
|
||||
{ file: "paper.jpg", asset: "Paper 003", id: "Paper003", use: "sheets, labels, tape, the report" },
|
||||
{ file: "carbon.jpg", asset: "Fabric 004", id: "Fabric004", use: "the keyboard deck" },
|
||||
{ file: "plastic.jpg", asset: "Plastic 012 A", id: "Plastic012A", use: "the drawer interior" },
|
||||
];
|
||||
|
||||
const FONTS = [
|
||||
{
|
||||
name: "Newsreader",
|
||||
by: "Production Type",
|
||||
use: "headlines, paper copy",
|
||||
use: "printed headlines, sheet copy, stamped numbers",
|
||||
href: "https://fonts.google.com/specimen/Newsreader",
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Sans",
|
||||
by: "IBM",
|
||||
use: "body text",
|
||||
use: "body text on the desk",
|
||||
href: "https://fonts.google.com/specimen/IBM+Plex+Sans",
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
by: "IBM",
|
||||
use: "labels, readouts, LEDs",
|
||||
use: "engraved labels, terminal text, index cards",
|
||||
href: "https://fonts.google.com/specimen/IBM+Plex+Mono",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Credits() {
|
||||
return (
|
||||
<div className="w-root">
|
||||
<main className="shell pb-20 pt-24">
|
||||
<p className="w-kicker">credits</p>
|
||||
<h1 className="w-serif mt-3 text-[2rem] sm:text-[2.6rem]">Everything borrowed, credited.</h1>
|
||||
<p className="w-prose mt-5 max-w-[62ch]">
|
||||
The web theme is built out of a few CC0 textures and open-licensed typefaces. Nothing here is
|
||||
scraped or unlicensed — everything below is free to reuse, and the authors are named even where
|
||||
the licence does not require it.
|
||||
<div className="w-page">
|
||||
<main className="mx-auto w-full max-w-[1120px] px-5 pt-24 pb-20 sm:px-8">
|
||||
<div className="w-sechead">
|
||||
<span className="w-mt-brass w-sechead__tab">credits</span>
|
||||
<h1 className="w-serif w-paint w-sechead__title">Everything borrowed, credited.</h1>
|
||||
</div>
|
||||
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]">
|
||||
The desk is built out of five CC0 textures and three open-licensed typefaces. Nothing is scraped
|
||||
or unlicensed — the authors are named even where the licence does not require it.
|
||||
</p>
|
||||
|
||||
<section className="mt-12">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="w-plate">textures</span>
|
||||
<h2 className="w-serif text-[1.3rem]">ambientCG — CC0 1.0 (public domain)</h2>
|
||||
<section className="mt-14">
|
||||
<div className="w-sechead !mb-6">
|
||||
<span className="w-mt-brass w-sechead__tab">textures</span>
|
||||
<h2 className="w-serif w-paint w-sechead__title !text-[1.3rem]">ambientCG — CC0 1.0</h2>
|
||||
</div>
|
||||
<p className="w-prose mt-4 max-w-[62ch]">
|
||||
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]">
|
||||
All five materials come from{" "}
|
||||
<a className="w-link" href="https://ambientcg.com" target="_blank" rel="noopener noreferrer">
|
||||
ambientCG
|
||||
</a>{" "}
|
||||
(Struffel Productions), released under the{" "}
|
||||
(Struffel Productions) under the{" "}
|
||||
<a
|
||||
className="w-link"
|
||||
href="https://creativecommons.org/publicdomain/zero/1.0/"
|
||||
|
|
@ -71,11 +72,11 @@ export default function Credits() {
|
|||
tint them in CSS.
|
||||
</p>
|
||||
|
||||
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||
<ul className="mt-7 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{TEXTURES.map((t) => (
|
||||
<li key={t.id} className="w-paper w-card">
|
||||
<div className="w-serif text-[1.02rem]">{t.asset}</div>
|
||||
<div className="mt-1 font-mono text-[0.7rem] leading-relaxed text-[var(--paper-ink-soft)]">
|
||||
<li key={t.id} className="w-mt-paper w-cardindex">
|
||||
<p className="w-serif text-[1.05rem] text-[var(--ink)]">{t.asset}</p>
|
||||
<p className="w-mono mt-2 text-[0.64rem] leading-relaxed text-[var(--ink-soft)]">
|
||||
source:{" "}
|
||||
<a
|
||||
className="w-link"
|
||||
|
|
@ -86,50 +87,54 @@ export default function Credits() {
|
|||
ambientCG / {t.id}
|
||||
</a>
|
||||
<br />
|
||||
licence: CC0 1.0 · used as <code>{`/textures/${t.file}`}</code>
|
||||
</div>
|
||||
licence: CC0 1.0
|
||||
<br />
|
||||
used as <code>{`/textures/${t.file}`}</code> — {t.use}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-14">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="w-plate">type</span>
|
||||
<h2 className="w-serif text-[1.3rem]">Google Fonts — SIL Open Font License 1.1</h2>
|
||||
<section className="mt-16">
|
||||
<div className="w-sechead !mb-6">
|
||||
<span className="w-mt-brass w-sechead__tab">type</span>
|
||||
<h2 className="w-serif w-paint w-sechead__title !text-[1.3rem]">Google Fonts — SIL OFL 1.1</h2>
|
||||
</div>
|
||||
<ul className="mt-6 grid gap-3 sm:grid-cols-3">
|
||||
<ul className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{FONTS.map((f) => (
|
||||
<li key={f.name} className="w-paper w-card">
|
||||
<div className="w-serif text-[1.02rem]">{f.name}</div>
|
||||
<div className="mt-1 font-mono text-[0.7rem] leading-relaxed text-[var(--paper-ink-soft)]">
|
||||
by {f.by} · {f.use}
|
||||
<li key={f.name} className="w-mt-paper w-cardindex w-cardindex--alt">
|
||||
<p className="w-serif text-[1.05rem] text-[var(--ink)]">{f.name}</p>
|
||||
<p className="w-mono mt-2 text-[0.64rem] leading-relaxed text-[var(--ink-soft)]">
|
||||
by {f.by}
|
||||
<br />
|
||||
{f.use}
|
||||
<br />
|
||||
<a className="w-link" href={f.href} target="_blank" rel="noopener noreferrer">
|
||||
fonts.google.com
|
||||
</a>{" "}
|
||||
· OFL 1.1
|
||||
</div>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-14">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="w-plate">notes</span>
|
||||
<h2 className="w-serif text-[1.3rem]">Other marks</h2>
|
||||
<section className="mt-16">
|
||||
<div className="w-sechead !mb-6">
|
||||
<span className="w-mt-brass w-sechead__tab">notes</span>
|
||||
<h2 className="w-serif w-paint w-sechead__title !text-[1.3rem]">Other marks</h2>
|
||||
</div>
|
||||
<p className="w-prose mt-4 max-w-[62ch]">
|
||||
The social buttons use brand marks that remain the property of their owners and are used only
|
||||
to link to those accounts. The project and hardware text is my own. The desktop mode (KDE
|
||||
sim) uses separate assets credited inside it.
|
||||
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]">
|
||||
The social ports use brand marks that remain the property of their owners and are used only to
|
||||
link to those accounts. Everything written on the paper is mine. The desktop mode (the KDE
|
||||
simulation) uses its own separate assets.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="mt-14">
|
||||
<Link href="/" className="w-link font-mono text-[0.8rem]">
|
||||
← back to the rack
|
||||
<p className="mt-16">
|
||||
<Link href="/" className="w-link w-mono text-[0.78rem]">
|
||||
← back to the desk
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
|
|
|
|||
1492
src/app/web.css
1492
src/app/web.css
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue