Warm-paper sticker desk. Three pages: the desk (home), things, about. Locked design canvas: https://claude.ai/code/artifact/e663991f-cf55-4551-a7cc-8dd017cb7bd9
npm install
npm run dev # http://localhost:3000
npm run build
npm run typecheck-
UI components come from beUI only — https://beui.dev/components/motion Copied verbatim into
components/motion/(React + TS + Tailwind + Motion, the library's own delivery model). Do not hand-roll a control that beUI already ships; if a new interaction is needed, take it from that list first. Shared motion tokens live inlib/ease.ts— every spring in the app comes from there, so the whole site moves with one set of physics. -
The 3D desk and the cast are bespoke art, not UI.
components/desk/andcomponents/art/are the illustrated layer. Rule of thumb: if it takes input, it's beUI; if it's a drawing, it's ours. -
The design system is locked (
app/globals.css): paper#f4eee2· ink#241f1c· orange#e8562f(voice) · blue#2f6fed(the route) · yellow#ffc53a· green#6fae7b. Type: Bricolage Grotesque (display) / Instrument Sans (body) / Shantell Sans (margin notes) / Geist Mono (labels). Self-hosted inapp/fonts/. Spacing: 76px gutters, ~200px between sections, 30–34px grid gaps. -
The sticker is the atom. Every interactive thing is a
<Sticker>(components/site/sticker.tsx): white bleed ring + contact shadow, ±3° tilt. Three decorative stickers per screen, max. One margin note per area.
app/ routes + tokens + self-hosted fonts
components/
motion/ beUI components, verbatim
site/ compositions built from beUI + the design system
art/ the SVG cast (avatar, cat, mug, plant, plane…)
desk/ the R3F hero scene (code geometry, no model pipeline)
config.ts WHAT is on the desk: props, positions, doors, cat spots, camera
rig.tsx renders a layout; nothing in it names a specific object
lamp.tsx the lamp and the physics of switching it (see below)
lights.tsx the room's light, crossfading with the lamp
objects.tsx the props; cat.tsx the cat; primitives.tsx ink + labels
lib/ cn(), motion tokens, hooks
The site is a static export (output: "export"), so Pages can serve it as-is.
.github/workflows/deploy.yml builds on every push to main and works for
either repo shape — it derives basePath from the repo name, so a user site
(iamartyaa.github.io) serves from / and a project repo from /<repo>.
First time, from this folder:
# 1. create the repo on github.com (empty, no README), then:
git remote add origin https://github.com/iamartyaa/iamartyaa.github.io.git
git push -u origin main
# 2. on GitHub: Settings → Pages → Source: "GitHub Actions"That's it — the workflow does the rest, and every later push redeploys.
- Design system, fonts, tokens, night mode (View Transition wipe, lamp-lit 3D)
- Home — hero, 3D desk, marquee, "what's on it", teasers, contact
- /things — the route, four landings, the drawer
- /about — the pilot, the shelf, "where I've flown", how I work, say hi
- Real content from the résumé
- Open Graph card (
public/og.png) +app/icon.svg - The portrait peels off and can be stuck anywhere in the band; the face
reacts through the gesture (
components/site/peel-sticker.tsx) - The desk spins a true 360 and every object sits on the surface
- Night mode is the lamp's pull chain on Home, and the cord hanging off
the top edge everywhere else (
components/site/pull-cord.tsx) - The cat's eyes follow the cursor on /about, and he is the mascot that
follows it around /things (
components/site/cursor-cat.tsx) - The say-hi card is signed by hand (
components/site/signature.tsx) - /writings — the shelf, plus The GEMM Scrapbook served verbatim in its own design system (see below)
- Traffic plumbing: sitemap.xml, robots.txt, RSS at /feed.xml, per-article canonical + OpenGraph + TechArticle JSON-LD
- Push to GitHub and turn Pages on — the two steps that need your login
- Drop a
resume.pdfintopublic/— the "résumé" sticker links to it
components/desk/config.ts is the only file that knows what stands on the desk.
HOME_LAYOUT.props is a list of { kind, position, hotspot? }; add a line and
the rig draws it, wraps it in a hover-and-click door if it has a hotspot,
and the cat's landing spots are the same layout's catSpots. A second desk
for another page is a second layout passed to <Desk layout={…} />.
CAMERA.fit is the size the canvas has to hold; the rig scales the scene so
that fits both the visible width and height, which is what keeps a phone's
short landscape canvas and a desktop's tall column showing the same
composition. As the hero scrolls away the desk tips toward top-down
(CAMERA.scrollTilt).
Pull the chain and the lamp does what a lamp does (lamp.tsx): the filament
warms up over ~0.4s with three dips as the contact settles, the shade and bulb
glow, the pool of light spreads on the wood, the shade rocks from the tug,
and the lamp becomes the only shadow-caster — the sun's shadows fade out as
the lamp's fade in, so every shadow on the desk swings round. Off is a fast
decay with one after-glow blink. It is one curve driven by time-since-pull,
not keyframes, so hammering the chain looks right too. At night the monitor
also spills onto the keycaps.
The island (desk.tsx) stops rendering while it is off-screen and, on a
touch device, drops the shadow maps, the contact-shadow pass and the 2×
pixel ratio.
Below md the route stickers live in a dock at the bottom of the screen
(thumb reach; it tucks away while you scroll down and comes back on the way
up) and the pull cord sits in the dock. Nothing from the cast is removed on
touch: the portrait, the trait stickers, the pilot cluster (eyes follow the
last tap), the timeline walker and the route mouse all stay, a size down.
Section rhythm comes from --spacing-section / -sm / --spacing-page
(clamps in globals.css), never a fixed rem.
| Where | What it does |
|---|---|
| Home, the desk | drag to spin 360°, hover an object for its label, click to go there |
| Home, the keyboard | a wave of keycaps depresses and the monitor types a line |
| Home, the cat | hops between three spots on the desk, and says so |
| Home, the lamp chain | pulls night mode on and off; the lamp warms up, the shadows swing, the wipe starts at the lamp |
| Home, the drawer | click to slide it open (the small things are in it); click again to go to them |
| Home, the mug | tips toward you when you reach for it; click for say-hi |
| Home, the portrait | peel it off, drop it anywhere in the band, double-click to put it back |
| Home, the trait stickers | pick up and throw (drag momentum) |
| /things, the filters | really filter, and the dashed line is re-generated through what's left |
| /things, scrolling | a mouse runs the line pinned to the middle of the screen; the cat chases it, never catches it, and loses it behind the drawer |
| /about, the portrait | eyes track the cursor; waves, stretches and drinks chai on its own clock; poke it for one on demand |
| /about, the cat and mug | pupils track the pointer; click for a hop or a sip |
| /about, the timeline | a figure walks it and changes costume at every stop |
| /about, the guestbook | press the stamp for the real visitor count |
| Anywhere but Home | the cord: drag it down or click it |
GoatCounter (lib/analytics.ts), cookieless and storing nothing personal —
which is why there is no consent banner anywhere on the site. The script goes
on every page including the standalone article; the guestbook on /about reads
the public TOTAL.json back. If the count can't be read the stamp still works
and claims no number rather than showing a zero.
- Accents have text twins (
--orange-inkand friends). The fills measure 1.6:1 (yellow) to 4.1:1 (blue) at label size, so anything set in an accent uses the twin. - Large inverted panels use
--panel, which stays dark in both themes. An ink sticker flipping to paper is right at 40px and a floodlight at 400px. :focus-visibleis the route's blue at 3px with an offset, because the browser default vanishes inside a sticker's 5px paper ring.- Labels (
.label) are 11.5px — 10px failed the legibility floor on phones. Orange stickers carry ink text (5.3:1); white on that orange was 3.6:1. - Known gap: the 3D desk's hotspots are not in the tab order. Every destination they open is also a text link in the nav and in "What's on it", so nothing is only reachable by pointer — but the objects themselves are mouse-only.
components/site/route.tsx owns the blue dashed line and the plane that flies
it. The path lives in a 0 0 1440 <height> viewBox stretched over the section,
so a waypoint always lands on the card it belongs to; the plane is sampled off
that same path (getPointAtLength, pre-sampled once) and positioned in
percentages, with its heading taken from the tangent so it banks into turns.