From 709fba188c73ee971225ece0115ad2ddbda89b4b Mon Sep 17 00:00:00 2001 From: dvorakmortonxd Date: Wed, 23 Sep 2026 23:57:22 +0200 Subject: [PATCH] billions must vibecode! - opencode.md creation --- opencode.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 opencode.md diff --git a/opencode.md b/opencode.md new file mode 100644 index 0000000..e3abf2a --- /dev/null +++ b/opencode.md @@ -0,0 +1,192 @@ +# opencode.md — cold-boot notes + +Quick context so a fresh session (no memory of this project) can be productive in +a minute. Read this first, then skim the files it points at. + +--- + +## What this project is + +Personal portfolio site for **dvorakmortonxd.dev**. It's not a normal website — +it ships **two fully interactive desktop/OS simulations** plus a terminal, and +picks which one to show based on theme and device. + +- Framework: **Next.js 16** (App Router, Turbopack), **React 19**, TypeScript. +- Styling: plain CSS files (`page.css`, `skeuo.css`), no CSS-in-JS, no Tailwind + utilities in the components (Tailwind is present via PostCSS but the look is + hand-written CSS). +- Path alias: `@/*` → `src/*`. +- Version: `2.1.0` (see `package.json`). + +### Scripts +```bash +npm run dev # next dev --turbopack (a server may already be running on :3000) +npm run build # next build --turbopack +npm run start # next start +npm run lint # eslint +``` +Useful checks before handing work back: `npx tsc --noEmit`, `npx eslint `, +`npm run build`. + +### Git +Branch is **main**. Remote is a self-hosted Gitea at +`git.dvorakmortonxd.dev`. **The remote URL contains a plaintext access token.** +Do not print it, copy it into new files, or push it anywhere else. + +--- + +## Architecture + +``` +src/app/ + page.tsx → renders , imports page.css + skeuo.css + layout.tsx → html shell, ThemeScript + data.ts → friends[], WALLPAPER, MOBILE_WALLPAPER (static content) + page.css → base / terminal theme styles + skeuo.css → Windows/Plasma desktop + mobile home styles + components/ + ThemeRoot.tsx → picks which shell to show (the router of the UI) + TerminalLayout.tsx → "terminal" theme (Gruvbox) + WindowsDesktop.tsx → "skeuo" theme, desktop: icons, windows, taskbar + MobileHome.tsx → "skeuo" theme, phone: app grid + dock + WinWindow.tsx → a single draggable/resizable window + apps.tsx → APPS registry + the content of each app + useDesktopIcons.ts → desktop icon grid + drag/drop (THE KEY FILE, see below) + useDragOrder.ts → simple list reordering (mobile home screen only) + useTheme.ts → theme state via useSyncExternalStore + ThemeScript.tsx → inline pre-hydration script: theme + device detection +``` + +### How UI selection works +1. `ThemeScript` runs before paint. It reads `localStorage.theme` and sets + `data-theme` (`skeuo` | `terminal`) and `data-device` (`phone` | `desktop`) + on `` from a user-agent sniff. +2. `useTheme()` (in `useTheme.ts`) reads `data-theme` via + `useSyncExternalStore` and dispatches a `theme-change` event on toggle. +3. `ThemeRoot` shows the terminal when `theme === "terminal"`; otherwise it + mounts **both** `WindowsDesktop` and `MobileHome` and lets CSS + (`html[data-device="phone"]`) decide which is visible. The desktop stays + mounted while hidden so the Welcome window isn't recreated. + +Themes: `skeuo` = KDE Plasma / Windows desktop, `terminal` = Gruvbox terminal. + +--- + +## `useDesktopIcons.ts` — the desktop icon grid (read this carefully) + +This is the most edited and most subtle file in the repo. It owns desktop icon +placement: both the default layout and drag-and-drop. If something is wrong +with icons, it's almost certainly here. + +### Design +- The three icon groups (apps on the left, services top-right, friends + bottom-right) are **real flex stacks** in the DOM at `.win-icons` (plus + `.win-icons-tr` / `.win-icons-br`). They are rendered **hidden** with class + `win-measure` purely so the grid can be measured from them. +- On mount (a `useLayoutEffect`), `measure()` reads the real rendered boxes of + those stacks: cell size = icon width/height **+ the CSS flex gap** (`GRID_GAP`, + currently 4px), and origin = the left stack's offset. From that it computes + `cols` / `rows` for the whole desktop. +- `defaultCells()` assigns each icon the cell its stack already occupies. So + **the grid you see on load is the grid dragging uses** — no re-layout jump. +- Icons are then rendered absolutely positioned in `.win-icon-layer`, driven by + the `positions` map (`{ [iconId]: {x, y} }`). +- Dragging: `onPointerDown` builds a **ghost** (a clone of the button appended + to `document.body`, moved with `translate()` under a `grabbing` cursor). The + ghost is built imperatively on purpose — moving it must not re-render React. +- Drops snap to a cell. `normalize()` is the single guard that enforces + **one icon per cell**: it assigns icons in a stable order and bumps any + collision to the nearest free cell. Every mutation path (initial layout, + drop, window resize) goes through it. + +### Invariants — do not break these +1. **One icon per cell.** All position mutations must run through `normalize()`. +2. **The grid must not change shape on first drag.** Default cells come from + measuring the real layout; don't hardcode cell sizes. +3. **The init effect must run exactly once.** It depends only on `iconKeys`, + which is the module-level constant `ICON_KEYS` in `WindowsDesktop.tsx`. If + you pass a fresh array literal here you WILL get "Maximum update depth + 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 +**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". + +Guard (already in place, keep it that way): +- `draggable={false}` on the `` in `renderIcon`. +- CSS `-webkit-user-drag: none` on `.win-icon` and its children. +- CSS `pointer-events: none` on `.win-icon-glyph img` / `.win-icon-label` so + `e.target` is always the button. + +### Related +`useDragOrder.ts` is a **different, simpler** hook used only by `MobileHome` for +reordering the phone app grid/dock (drag one item over another to swap). It is +list reordering, not free placement. Don't confuse the two. + +--- + +## History of the desktop-icon work (why the code looks like this) + +1. **Free placement.** First version let icons go anywhere. Worked, but icons + could overlap and the layout jumped into a grid on first drag. +2. **Grid snapping.** Dropped icons snap to a grid cell; dropping onto an + occupied cell bumps the occupant. +3. **Grid bug fixes.** The cell pitch originally omitted the 4px flex gap, and + the drop calculation rounded differently from the occupancy check, so two + icons could land in the same cell. Fixed by deriving pitch from measured + boxes **plus gap**, and by routing everything through `normalize()`. +4. **Layout no longer jumps.** Grid measured from the real stacks and applied + before paint, so load layout == drag layout. +5. **Infinite loop fix.** `iconKeys` was an inline array literal in the effect + deps → the effect re-ran every render → `setState` → loop. Hoisted to the + module constant `ICON_KEYS` and dropped the unstable callbacks from deps. +6. **Friend icons couldn't be dragged.** Native image drag on the + `next/image` glyph fired `pointercancel`. Fixed with `draggable={false}` + + `-webkit-user-drag: none`. + +--- + +## Conventions & things that will bite you + +- Components are client components (`"use client"` at the top) — they use + pointer events and window APIs freely. +- CSS classes are prefixed by area: `win-` (desktop), `mobile-` (phone), + `app-` / `kickoff` / `terminal` etc. Grep by prefix to find styles. +- Several places deliberately use **imperative DOM work** (drag ghost, + transform-origin for the mobile launch animation). This is intentional for + performance; prefer matching the existing pattern over "Reactifying" it. +- `setState` inside an effect is blocked by the ESLint config + (`react-hooks/set-state-in-effect`). The one intentional exception is the + grid init in `useDesktopIcons.ts`, which carries a scoped + `eslint-disable-next-line` with a comment. Don't add more without a reason. +- Watch for `pointercancel`: any custom pointer gesture over an ``, link, + or text selection needs `draggable=false` / `user-select: none`. +- Reduced motion is handled with a near-zero `animation-duration` (not + `animation: none`) so `animationend` still fires and windows unmount. + +## Testing notes +- There is **no test suite**. Verification is `tsc --noEmit`, `eslint`, and + `next build`. +- A dev server may already be running on port 3000; check before starting one. +- The desktop layout is geometry-heavy. When changing grid maths, a small + standalone script mirroring `normalize()` / `posToCell()` is a fast way to + prove one-icon-per-cell holds for edge cases (offscreen drops, tiny + viewports, repeated drops on one cell). + +--- + +## After finishing — push to git + +When the work is done, run **only** these three commands (no extra flags, no +rebasing, no force-push): + +```bash +git add . +git commit -m "" +git push origin main +``` + +Replace `` with a short, specific summary of the change.