dvorakmortonxd.dev/opencode.md

9 KiB

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

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 <files>, 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 <ThemeRoot/>, 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 <html> 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 <img> (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 <Image> 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.

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 <img>, 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):

git add .
git commit -m "<WHAT CHANGED>"
git push origin main

Replace <WHAT CHANGED> with a short, specific summary of the change.