Skip to content

Fix inaccuracies in CLAUDE.md and add desktop/mobile implementation plans - #5

Open
radroid wants to merge 222 commits into
mainfrom
claude/reorganize-portfolio-structure-ARXUE
Open

Fix inaccuracies in CLAUDE.md and add desktop/mobile implementation plans#5
radroid wants to merge 222 commits into
mainfrom
claude/reorganize-portfolio-structure-ARXUE

Conversation

@radroid

@radroid radroid commented Apr 29, 2026

Copy link
Copy Markdown
Owner

CLAUDE.md: correct boot description, BootSequence→BootScreen component name,
and .woff2→.woff font path. Add pointer to DESKTOP-PLAN.md.

DESKTOP-PLAN.md: fix container-type (inline-size→size for cqh support),
correct Phase 4 branching (apps branch from mac-os-1984-desktop, not
pre-app-foundation), fix branch tree diagram, drop redundant isSelected
from WindowState, collapse isMaximized two-step into direct context usage.

MOBILE-PLAN.md: initial planning document for iPhone OS 1 mobile experience.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

radroid and others added 30 commits April 8, 2026 21:21
…lans

CLAUDE.md: correct boot description, BootSequence→BootScreen component name,
and .woff2→.woff font path. Add pointer to DESKTOP-PLAN.md.

DESKTOP-PLAN.md: fix container-type (inline-size→size for cqh support),
correct Phase 4 branching (apps branch from mac-os-1984-desktop, not
pre-app-foundation), fix branch tree diagram, drop redundant isSelected
from WindowState, collapse isMaximized two-step into direct context usage.

MOBILE-PLAN.md: initial planning document for iPhone OS 1 mobile experience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mobile gate

- Window positions stored as container percentages (0–1) instead of pixels
  so they scale automatically on maximize toggle
- isMaximized state lifted from WindowManager context to page.tsx,
  passed as props to IMacG3Frame and Desktop
- Maximize trigger simplified to chin button only (removed menu bar option)
- Clarified mobile never advances past welcome phase
- Drag listeners attached to document to prevent fast-drag cursor loss

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
POSTHOG-RESEARCH.md: architectural reference notes on PostHog.com's
desktop-OS implementation (state model, window chrome, desktop shell,
styling primitives). Compiled via parallel subagent exploration of
the posthog.com repo.

DESKTOP-PLAN.md updates inspired by the research:
- Derived focused window (no stored activeWindowId) — top of z-stack
  IS the focused window via useMemo
- Contiguous zIndex reshuffle on focusApp (no counter drift)
- 5px click-vs-drag threshold on draggable title bars
- Lazy content mount after window entry animation
- Zoom-from-origin open animation using clicked-icon Rect
- Cascade-on-open position spec and close-normalizes-zIndex rule
- Menu bar z-index sits above window stack
- Reduced-motion covers maximize + open + close animations
- Inspiration & Anti-patterns section (7 patterns adopted, 9 rejected,
  4 future enhancements)
- Readiness Checklist marking PR #1 ready and app content blockers

CLAUDE.md: note that dev server is always running and should not
be started by automation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the `'desktop'` screen phase and maximize mode as the
foundation for the interactive Mac OS 1984 desktop experience.

Screen phases:
- ScreenPhase gains `'desktop'`
- Welcome screen renders without its menu bar (menu bar moves to the
  desktop phase in Phase 2+3)
- Desktop viewports auto-advance from `'welcome'` → `'desktop'` after
  a brief hold; mobile stays on welcome permanently

Maximize mode:
- `isMaximized` state lifted to `page.tsx`, passed to IMacG3Frame and
  CRTScreen as props (per DESKTOP-PLAN architecture)
- Chin button on the iMac frame toggles maximize; a floating Restore
  button appears when maximized so the control stays reachable after
  the chin hides
- IMacG3Frame hides body/chin/stand when maximized; CRTScreen expands
  to fill the viewport
- CRTScreen gains `container-type: size` to enable cqw/cqh-based
  window sizing in later phases
- All transitions respect prefers-reduced-motion

Containing-block fix:
- The initial intro scale animation is released after 700ms (new
  `introDone` state), so a persistent `transform: scale(1)` no longer
  creates a containing block that traps the maximized IMacG3Frame's
  `position: fixed, inset: 0`.

Assets (user-provided):
- public/app-icons/: 1-bit SVGs for calculator, control-panel, finder,
  journal, music, notepad, trash, world-map (browser icon pending)
- public/apple-icon.svg
- public/cv/Raj_Dholakia_Resume_FullStack.pdf

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the full interactive desktop — draggable windows,
placeholder apps, icon grid, and a dynamic menu bar that adapts to
the focused app. This is the last piece of the foundation PR before
app PRs can branch off in parallel.

Window manager (app/components/desktop/window-manager.tsx)
- React Context holding a windows record, selectedIconId, and an
  hasOpenedAnyApp flag
- Derived activeWindowId via useMemo — top of z-stack is the focused
  window; no separate focus field to sync
- focusApp does a contiguous zIndex reshuffle (no counter drift)
- closeApp re-normalizes zIndex so gaps never appear
- Cascade-on-open: first window at {12%, 14%}, +3% per next, wraps
  back to base past {60%, 55%}
- moveWindow clears fromOrigin so the zoom animation can't replay

App registry (app/components/desktop/app-registry.tsx)
- Typed AppDefinition/MenuConfig/MenuItem/Rect
- All 8 apps registered with the user-provided 1-bit SVG icons
  (browser.svg now included)
- clamp() + cqw/cqh sizes per the DESKTOP-PLAN
- Shared ComingSoon placeholder component until Phase 4 replaces it
- FINDER_DEFAULT_MENUS exported for the fallback menu bar state

Window component (app/components/desktop/window.tsx)
- Title bar with striped-active pattern, close box with an inline-SVG
  X, focus-on-click
- Drag uses direct DOM transform: translate3d() during the move,
  rAF-throttled; only the final position is committed via moveWindow
  on mouseup. Avoids re-rendering every context consumer per frame.
- 5px click-vs-drag threshold so taps don't jitter the window
- One-frame `transition: none` skip on drag commit so the window
  snaps to the new position without a ghost animation
- Zoom-from-origin open animation: first render at the clicked icon's
  CRT-relative rect, then flipped to the cascaded position on the
  next frame so CSS interpolates the delta
- Lazy content mount — app component only renders after the entry
  animation completes (skipped under prefers-reduced-motion)
- Drag is constrained to the CRT screen bounds via container ref

DesktopIcon (app/components/desktop/desktop-icon.tsx)
- Single click selects (invert colors), double click opens and
  captures the icon's rect for the zoom-from-origin animation
- `large` prop scales the icon/label up when the desktop is maximized

Desktop shell (app/components/desktop/desktop.tsx)
- Wraps children in WindowManagerProvider
- 2-column icon grid in the top-right so all 8 apps fit in the small
  CRT; icons scale up in maximize mode
- Decorative Trash bottom-right with pointer-events: none
- Windows layer renders every open window; click on bare desktop
  deselects the active icon

MaximizeNudge (app/components/desktop/maximize-nudge.tsx)
- Small Mac-style tip dialog that appears once after the first app
  opens, suggesting Full Screen. Auto-dismisses when user maximizes
  or clicks Not Now. Persisted to sessionStorage.

Menu bar refactor (app/components/menu-bar.tsx)
- Now reads derived activeWindowId from WindowManagerContext
- Apple-glyph menu always leftmost (SVG apple)
- Falls back to Finder defaults when no window is focused
- Per-app menuItems (when provided) override the defaults
- z-index 9999 so dropdowns cover windows
- No longer mounted in welcome-screen.tsx

page.tsx
- Desktop phase now renders <Desktop /> (the real shell) instead of
  the Phase 1 placeholder text
- Under-construction indicator removed from the welcome screen

Assets
- public/app-icons/browser.svg added to round out all 8 app icons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trash was previously a decorative element pinned bottom-right with
pointer-events: none. It's now a real entry in APP_REGISTRY with a
Coming Soon placeholder component and a Finder-style status bar.

Desktop shell renders Trash as a normal DesktopIcon at bottom-right
(filtered out of the top-right 2-column grid so layout is preserved),
so single-click selects and double-click opens with the usual
zoom-from-origin animation.

DESKTOP-PLAN.md Decision #11 updated to match — Trash is clickable
but not functional beyond opening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scaffolds the shadcn/ui context-menu component (app/components/ui/
context-menu.tsx) and wires it into desktop icons and the bare
desktop surface.

shadcn component
- Installed via `shadcn add context-menu` which added
  @radix-ui/react-context-menu to package.json
- File rewritten to drop the lucide-react dependency and the
  Tailwind v3 theme tokens (bg-popover, rounded-md, animate-in, …)
  that don't exist in our Tailwind v4 alpha setup
- Same export surface as stock shadcn, restyled inline with Mac OS 1
  aesthetics: Chicago font, white bg, 1px black border, 2px black
  drop shadow, text glyphs (▶, ✓, ●) in place of lucide icons
- Hover/keyboard-focus highlighting driven by Radix's data-highlighted
  attribute via a small rule in global.css

Right-click handlers
- DesktopIcon: App name label → Open, Get Info, Duplicate, Move to
  Trash. Right-click also selects the icon so it shows the highlight
  state before the menu opens. Icon is split into DesktopIcon
  (handles context menu + data) and IconButton (forwardRef button
  used by ContextMenuTrigger asChild).
- Desktop surface: Curly OS label → Go Full Screen / Restore Screen
  (live-toggles based on isMaximized), Clean Up Desktop, Change
  Wallpaper, About This Macintosh. Wrapped the desktop container in
  ContextMenu/ContextMenuTrigger so empty-area right-clicks work.

Icon highlight fixes
- whiteSpace: normal on the label so two-word names (Curly Browser,
  Control Panel, Note Pad, World Map) wrap at the space and the
  black selection background covers the whole label instead of
  overflowing the button width
- outline: none on the icon button so the browser's default blue
  focus ring doesn't appear after a right-click
- Removed the filter: invert(1) on icon images when selected — the
  label highlight alone is enough; the icon art stays untouched

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Boot icon
- SIZING.macIconSize reduced ~30% (100 → 70 desktop, 60 → 42 mobile)
  so the Happy Mac on the boot screen sits more comfortably in the
  CRT frame.

Chin toggle
- The chin used to be stacked (CD slot on top, separate circular
  button below) and the button was only rendered on welcome/desktop
  phases. This caused a visible chin-height shift when the screen
  transitioned out of boot. Now there is no separate button — the
  CD slot itself IS the maximize toggle. Always rendered (disabled
  during boot) so the chin height is constant across phases.
- Hover brightens the slot with a soft white glow to hint at
  interactivity without breaking the iMac hardware aesthetic.
- aria-label flips between "Maximize screen" and "Restore screen".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation: Mac OS 1984 desktop — maximize, windows, shell, menus, context menus
Lock in Browser (bookmarks launcher, no iframes), Finder (public/
mirror with Mac-styled folders), World Map (react-simple-maps + 24
visited countries), and Music (Spotify live now-playing with vinyl
presentation). Update readiness checklist — all 8 apps unblocked;
Music gated only on Spotify OAuth setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-installed on base so all app branches inherit the deps without
each needing its own install commit. react-simple-maps wraps d3-geo
and handles TopoJSON rendering; world-atlas provides the country
TopoJSON data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the original parallel-worktree-per-app strategy with a single
branch (feat/desktop-apps → feat/mac-os-1984-desktop) containing one
commit per app. The previous plan assumed subagents could run git/tsc/
gh inside isolated worktrees; in practice the session sandbox denies
Bash for subagents, so the main agent drives every git operation while
subagents write component files only.

Also document the Spotify integration architecture in App 8: refresh-
token grant for runtime (no per-URL redirect registration), one-time
OAuth helper on 127.0.0.1:8888/callback for dev-time capture, and a
per-environment env-var table (.env.local / Cloudflare Preview /
Production) so the same route.ts works everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node 18+ ESM helper. Reads SPOTIFY_CLIENT_ID/SECRET from .env.local,
starts a loopback server on 127.0.0.1:8888, opens the browser to
Spotify's authorize endpoint, and on /callback exchanges the auth
code for a refresh token. Appends SPOTIFY_REFRESH_TOKEN to .env.local
if not already present.

The callback URL is used only for this one-time ritual — at runtime
the Music app uses the refresh token server-side via the refresh_token
grant, so no redirect URI registration is needed for Cloudflare preview
or production deploys.

Run: node scripts/spotify-oauth.mjs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four-column grid with C / ÷ × − + operators, = spanning two rows on
the right, left-to-right chain evaluation matching the original Mac
calculator (2 + 3 * 4 = 20), division-by-zero → "Error", keyboard
shortcuts, and authentic 1-bit black/white button invert on :active.
No chrome, lives inside the generic Window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inline SVG zigzag for the torn top edge (stretches across any width
via viewBox + preserveAspectRatio), repeating-linear-gradient for the
lined paper background, and an auto-saving textarea persisted to
localStorage key `curly-os-notepad`. Pre-populates with a welcome
note on first load; empty string is respected once the user clears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stacked panel sections: user card (avatar, name, role, location,
contact links), live clock updating every second, system diagnostics
(browser, OS, screen resolution, window size, language, connection
type, timezone), Battery Manager readings with charging/levelchange
listeners, and derived location from Intl.DateTimeFormat timezone.
All runtime data comes from browser APIs — no network calls. Battery
section degrades gracefully on Firefox/Safari where the API is absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Classic icon grid with folder navigation (path stack + Back button),
Mac-styled labels (Applications ← app-icons, Documents ← cv, Fonts,
Developer ← tech-icons), and an internal status bar that stays in
sync with the current folder. Double-click opens a file preview
overlay inside the Finder window: images/SVGs via <img>, audio via
<audio>, PDFs via <iframe> (browser native viewer), fonts show a
sample, unknowns fall back to "not available". Right-click provides
Open, Download, Get Info (disabled), Rename (disabled) via the
existing Shadcn ContextMenu.

Also hoists FINDER_DEFAULT_MENUS above APP_REGISTRY so the finder
entry can reference it without a temporal-dead-zone error, and drops
the unused `statusBar` fallback since the component renders its own.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User feedback on the first live screenshot was that the content was
hard to read at the default sizing. Bumps all shell and app font
sizes up one Chicago-friendly step (~10%):

  shell
  - desktop-icon: label 10→11 / 12→13, icon image 30→33 / 44→48,
    tile width 82→90 / 96→106, gaps 2→3 / 4→5
  - window title bar: 11→12, status bar: 10→11
  - menu bar default fontSize 12→13, height 22→24

  apps
  - calculator: button 13→14, display 20→22
  - note-pad: textarea 13→14, line-height 20→22, lined-paper
    gradient bumped to match
  - control-panel: section-header / label / value 11→12,
    user name 13→14, link buttons 10→11
  - finder: folder/file labels 12→13, secondary 11→12, heading
    18→20, status bar 10→11, breadcrumb 13→14

Stays inside the 1-bit Chicago aesthetic — no hierarchy changes,
just a single step up on every hard-coded px size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Classic Mac Scrapbook-style paged viewer. Extracted the 3 blog posts
from CONTENT-ARCHIVE.md as inline data (no markdown renderer added —
body is rendered via <pre style="white-space:pre-wrap"> for a period-
accurate look). Nav footer with ◀ / ▶ arrows disabled at the ends, a
"1 / N" page counter, and Left/Right keyboard navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the inline-drawn folder SVG with the user-provided 1-bit
folder asset. Keeps the Mac 1 aesthetic consistent with the rest of
the app icons and fixes the earlier too-cartoony rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The active title bar's horizontal-stripes pattern used to go
edge-to-edge and touched the close box, making focused windows look
crowded and unlike the classic Mac OS 1 reference. Restructure the
title bar into an outer white wrapper with a 2px inset and an inner
row that carries the stripes; the close box and title text sit in
their own white-padded spans so the stripes never crowd them, and
the right side mirrors the close-box cluster width so the title
stays visually centered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The three blog posts were originally mis-dated on the previous
website — they should all be 2026. Fixes the inlined Scrapbook data
and the source CONTENT-ARCHIVE.md so anything pulling from it in the
future picks up the right year.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Control Panel avatar 56→84px so the user card reads at a glance
- Title bar stripes now run edge-to-edge horizontally (removed the
  outer left/right padding) so they meet the window border like the
  Mac OS 1 reference. Close box has 4px of white against the window
  edge and 6px against the stripes; title text has 10px cushion each
  side. Right side mirrors the close-box footprint to keep the title
  visually centered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Marks commits 1-6 (plan, Spotify helper, Calculator, Note Pad, Control
Panel, Finder) as done, logs the extra commits surfaced from reviewing
the live preview (font/icon polish, folder icon swap, title-bar stripe
fix, blog year correction, avatar bump), and leaves Browser / World
Map / Music as the remaining work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous fix still had a white cushion span wrapping the close box,
which created a solid white block from the window edge inward before
the stripes began. In the Mac OS 1 reference, the stripes fill the
title bar edge-to-edge horizontally and the close box is an opaque
white element sitting ON the stripes, with only a tiny padding inset
(4px) from the window border.

This rewire puts the padding on the stripe container itself — since
background-image extends through padding, stripes fill the 4px inset
too, and the close box + its transparent right-side mirror sit as
opaque / transparent elements on that background. The title text
still cuts a white hole via its own background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No iframes. Only view is a bookmarks home with 11 tiles: 8 projects
(Penguin Mail, ARK Experience, Bridger, Stella 56 Diamonds, Playground,
Couples Budget, 75 Creates, KayVee Gems) + 3 tools (Google, Claude,
ChatGPT). Click opens in a new real-browser tab via window.open;
right-click exposes Open in New Tab + Copy Link via the existing
Shadcn ContextMenu. Disabled Back/Forward buttons and a decorative
read-only address bar match the Mac 1 chrome. Tan info banner below
the toolbar explains the "iframes get blocked" quirk up front.
Favicons pulled from Google's S2 service and pixelated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses react-simple-maps + world-atlas (TopoJSON via jsdelivr CDN) to
render the Natural Earth country borders. Visited countries match
against geo.properties.name (with both common name variants for the
USA and Czechia to cover either world-atlas naming convention).

Visited fill is a 4×4 checkerboard SVG <pattern> (two 1×1 black
rects) — the canonical Mac OS 1 50% stipple, matching the worldmap
reference aesthetic. A hidden 0×0 <svg><defs> hosts a second pattern
id so the bottom-right legend swatch (which lives in a <div> outside
the map's SVG) can resolve its url(#id) fill. Hover tooltip follows
the cursor via position: fixed (survives the window's transform
container in maximized mode).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hovering a visited country now shows an inverted tooltip — black
background, white text, with a ✓ prefix — so the "you've been there"
status is instantly visible without having to match the dither fill
against the legend. Unvisited countries keep the plain white tooltip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drops the bottom-right legend (and the hidden legend-pattern SVG that
supported it) to reclaim that space. Switches from geoMercator to
geoEqualEarth which has a more compact aspect ratio — no wasted
polar stretch — and bumps the scale to 175 plus a small center shift
([10, 15]) so the populated latitudes fill the window. Sets
width/height to 800×400 so the viewBox matches the compact
projection shape instead of the default 800×600 which left ~200px
of empty vertical space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
radroid and others added 27 commits April 17, 2026 10:53
Trivial one-liners (handleOpen, openInNewTab, copyLink) inlined.
The inline 'opacity: 0.4, cursor: default' on disabled menu items was
inconsistent with every other disabled ContextMenuItem in the app
(finder/desktop/desktop-icon all use bare 'disabled'); global CSS
already handles the look via [data-disabled] + pointer-events: none.
…ech-icons

textareaRef was assigned but never read; the onFocus handler duplicated
outline: none already in the textarea's style.

public/tech-icons/ was empty; removed from filesystem and from the
finder folder-rename list in DESKTOP-PLAN (never implemented).
makePlaceholder was used once (for Trash); inlined as component:
() => <ComingSoon name="Trash" />. The bearer-auth + no-store options
pattern was duplicated 3 times in the spotify route; extracted as
bearerOpts(token) so multi-line fetch calls collapse to one line.
Phase 5 polish items were all marked done per commit 3939e93. The file
was never referenced from code or other docs and duplicated commit
history.
Both the <pattern> tag's 4 attrs (world-map) and the outer flex <div>
(not-found) fit comfortably on one line.
Sweep across 10 files — chicago, nav/chrome/tile base styles, content/item
styles in the context-menu wrapper, calculator btnBase, etc. All were
4–6 lines with no CSSProperties value that needed per-line separation.
Implementation step-by-step tables (Phases 1–5) and Subagent Contracts
template were execution artifacts for work now complete. Design
Decisions, Architecture, Applications specs, and Inspiration/Anti-
patterns retained. Updated ToC to match.
The PR #3 workflow steps and "Why This Strategy" rationale were
execution notes for completed work. Branch tree retained as the
canonical merge path.
Spotify route returns genres already sorted descending by count; the
client's defensive re-sort added no behavior. Pass data.genres directly.
…ne line

Across curly-browser/music/scrapbook/crt-screen/maximize-nudge/imac-frame/layout.
The verbose 'destructure + type' block form gained no clarity vs the
one-line signature.
Visitors opening Finder now see the Resume PDF as the first item
(top-left of the icon grid) alongside a single Assets folder
containing the existing Applications and Fonts sub-folders plus
the loose root-level files. Generalizes the Finder's navigation
to walk pathStack arbitrarily deep so Assets can hold nested
folders.

https://claude.ai/code/session_01M9ddciS17Eq3bLLyAsv7sB
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
raj-portfolio f0e07f5 Commit Preview URL

Branch Preview URL
Apr 29 2026, 04:20 PM

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Mac System 1 desktop environment with draggable, resizable windows and menu bar
    • Built-in applications: Calculator, Control Panel, Finder, Music (Spotify), Note Pad, Scrapbook, Curly Browser, World Map
    • Window maximize mode with adaptive layouts
    • Spotify now-playing integration with genre and artist tracking
  • Documentation

    • Desktop architecture and implementation specifications (DESKTOP-PLAN.md)
    • Future mobile experience planning (MOBILE-PLAN.md)

Walkthrough

The pull request introduces a comprehensive macOS-styled desktop environment with window management, a suite of productivity and entertainment applications, Spotify integration, and supporting documentation. New APIs, components, planning documents, and configuration files establish the foundation for a desktop interface with draggable windows, app registry, and multiple built-in apps.

Changes

Cohort / File(s) Summary
Documentation & Planning
.gitignore, CLAUDE.md, README.md, CONTENT-ARCHIVE.md, DESKTOP-PLAN.md, MOBILE-PLAN.md
Updated repository guidance, dev workflow, and UI boot experience; rebranded site positioning; added comprehensive desktop UX and mobile implementation planning; updated gitignore for dev environment files.
Window Management & Desktop System
app/components/desktop/window-manager.tsx, app/components/desktop/window.tsx, app/components/desktop/desktop.tsx, app/components/desktop/desktop-icon.tsx, app/components/desktop/maximize-nudge.tsx
Introduced context-based window manager with cascade positioning, z-index focus reshuffling, drag/resize handlers, and app lifecycle management; added Window component with zoom-from-origin animations and lazy content mounting; created desktop layout with icon grid and window rendering; added icon interaction and maximize promotion UI.
Application Registry & Definitions
app/components/desktop/app-registry.tsx
Centralized app metadata, UI configuration, sizing constraints, menu structures, and component registration; created APP_MAP lookup and ComingSoon placeholder.
Desktop Applications
app/components/apps/calculator.tsx, app/components/apps/control-panel.tsx, app/components/apps/curly-browser.tsx, app/components/apps/finder.tsx, app/components/apps/music.tsx, app/components/apps/note-pad.tsx, app/components/apps/scrapbook.tsx, app/components/apps/world-map.tsx
Added eight new app components: calculator with operator chaining and display formatting; control panel with system info tiles (battery, network, timezone, resolution); Curly Browser with bookmarks, iframe navigation, and address bar; Finder with file browsing, previews, and downloads; Music with Spotify now-playing display, vinyl animation, and genre/artist charts; NotePad with localStorage persistence and keyboard navigation; Scrapbook journal with multi-entry navigation; World Map with visited country tracking and tooltips.
Spotify Integration
app/api/spotify/now-playing/route.ts, scripts/spotify-oauth.mjs
Added API route for now-playing/last-played data with refresh token exchange, top genre/artist computation, and no-cache headers; added OAuth helper script for token acquisition with browser redirect and state validation.
Core UI Components & Framework
app/components/boot-screen.tsx, app/components/crt-screen.tsx, app/components/imac-frame.tsx, app/components/menu-bar.tsx, app/components/welcome-screen.tsx, app/components/ui/context-menu.tsx, app/global.css
Refactored BootScreen to remove isActive gating; updated CRTScreen with maximize state and animations; enhanced IMacG3Frame with fullscreen mode, collapse effects, and toggle button; rewired MenuBar to use app-driven registry instead of static menus; added Radix-based context menu styling; removed boot animations, added vinyl spin keyframe, updated menu styling.
Page Routing & State
app/page.tsx, app/components/types.ts
Updated main page with desktop flow state (introDone, isMaximized), auto-advance from welcome to desktop, maximize toggle control wiring, and BootScreen prop removal; extended ScreenPhase type to include 'desktop'; condensed SIZING constant structure.
Configuration & Metadata
app/layout.tsx, app/robots.ts, app/sitemap.ts, next.config.ts, components.json, package.json
Extracted TITLE/DESCRIPTION metadata constants; simplified robots/sitemap formatting; introduced cache-control header helper; removed unused path aliases; added Radix context menu, react-simple-maps, and world-atlas dependencies.
Utilities & Cleanup
app/lib/utils.ts, app/lib/use-reduced-motion.ts, app/not-found.tsx, types/mdx-raw.d.ts, planning-details.md (deleted), wrangler.toml (deleted)
Removed cn utility function; cleaned up blank lines in useReducedMotion; reformatted not-found JSX; deleted ambient MDX module declaration and old portfolio planning document; removed Cloudflare Worker configuration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Desktop
    participant WindowManager as WindowManager<br/>(Context)
    participant APP_MAP as APP_MAP<br/>(Registry)
    participant Window as Window<br/>(Component)
    participant App as App<br/>(Component)

    User->>Desktop: Click app icon
    Desktop->>WindowManager: openApp(appId)
    WindowManager->>APP_MAP: Look up app definition
    APP_MAP-->>WindowManager: Return AppDefinition
    WindowManager->>WindowManager: Calculate cascade position<br/>Allocate z-index<br/>Create window record
    WindowManager-->>Desktop: windows[id] updated
    Desktop->>Window: Render with fromOrigin
    Window->>Window: Trigger zoom-from-origin<br/>animation (2 RAF)
    Window->>Window: Lazy mount content<br/>(after animation)
    Window->>App: Render app component
    App-->>Window: Content rendered
    Desktop->>User: Window visible & animated

    User->>Window: Click window title bar
    Window->>WindowManager: focusApp(appId)
    WindowManager->>WindowManager: focusReshuffle()<br/>Move to max z-index<br/>Decrement others
    WindowManager-->>Desktop: windows z-indices updated
    Desktop->>User: Window brought to front
Loading
sequenceDiagram
    participant MusicApp as Music App<br/>(Client)
    participant Cache as Module Cache
    participant APIRoute as /api/spotify/<br/>now-playing
    participant Spotify as Spotify API
    participant MusicApp2 as Music App<br/>(Poll)

    MusicApp->>Cache: prefetchSpotify()
    Cache->>APIRoute: fetch() GET
    APIRoute->>APIRoute: Read SPOTIFY_CLIENT_ID<br/>SPOTIFY_CLIENT_SECRET<br/>SPOTIFY_REFRESH_TOKEN
    APIRoute->>Spotify: POST /token<br/>(refresh_token grant)
    Spotify-->>APIRoute: access_token
    APIRoute->>Spotify: GET /me/player/<br/>currently-playing
    APIRoute->>Spotify: GET /me/top/artists<br/>GET /me/top/genres
    Spotify-->>APIRoute: Track + Top data
    APIRoute->>APIRoute: Compute top 5 genres<br/>top 10 artists<br/>Normalize response
    APIRoute-->>Cache: JSON response<br/>(no-cache headers)
    Cache-->>MusicApp: Cached data
    MusicApp->>MusicApp: Render with vinyl,<br/>genres, artists
    MusicApp2->>MusicApp2: Poll every 10s<br/>Update display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops excitedly
A desktop burrows into being—
Windows cascade, apps bloom bright,
Spotify spins its vinyl song,
And curlycloud hops toward home at last! 🏠

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: documentation corrections in CLAUDE.md and addition of desktop/mobile planning documents.
Description check ✅ Passed The description is directly related to the changeset, detailing specific fixes to CLAUDE.md, corrections to DESKTOP-PLAN.md, addition of MOBILE-PLAN.md, and co-author attribution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/reorganize-portfolio-structure-ARXUE

Review rate limit: 2/3 reviews remaining, refill in 20 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/spotify/now-playing/route.ts`:
- Around line 129-133: Start the fetchTopData work before awaiting the Spotify
currently-playing fetch to run them in parallel: call fetchTopData(access_token)
and store the promise in topPromise prior to awaiting the currently-playing
request (nowRes) so both network calls overlap, then await/consume topPromise as
needed (e.g., await topPromise or use Promise.all with the currently-playing
response) when you need the results; locate the code around the nowRes variable
and the fetchTopData call to make this change.
- Around line 106-164: The GET handler can throw if any Spotify fetch (token
exchange, currently-playing, recently-played or fetchTopData) rejects, producing
a 500 and breaking the expected JSON shape; wrap the entire function body in a
try/catch and normalize all error paths to return the same JSON contract (error,
isPlaying:false, track:null or lastTrack, plus top-data defaults). Add
per-request timeouts (use AbortController or a timeout wrapper) for the token
fetch, currently-playing fetch, recently-played fetch and the parallel
fetchTopData call (reference bearerOpts, fetchTopData,
tokenRes/nowRes/recentRes, makeTrack) and ensure you catch/recover from
fetchTopData rejection (e.g., await topPromise inside try with a safe fallback)
so transient network/Spotify errors return the stable offline payload instead of
throwing.

In `@app/components/apps/calculator.tsx`:
- Around line 104-116: The global keyboard handler in useEffect (onKeyDown) must
ignore keystrokes when the calculator is not active; modify onKeyDown to
early-return unless the event target is inside the calculator or the calculator
has focus by using a container ref (e.g., add a React ref like containerRef and
attach it to the root element) and checking
containerRef.current?.contains(e.target as Node) (or use a prop/state like
isActive if present), then only call
handleDigit/handleDecimal/handleOperator/handleEquals/handleClear when that
check passes; keep the existing addEventListener/removeEventListener but update
the dependency list to include the new ref or isActive if needed.

In `@app/components/apps/control-panel.tsx`:
- Around line 56-60: The UA detection branch orders cause Opera (OPR/...) to
match the Chrome (/Chrome\/(\d+)/) case first; move the /OPR\/(\d+)/ check
before the /Chrome\/(\d+)/ check (or alter the Chrome pattern to explicitly
exclude OPR/Chromium) so Opera returns `Opera ${...}`; update the corresponding
match extraction calls (the existing ua.match usages) accordingly to use the
correct branch for `OPR\/(\d+)` and keep the other checks for /Chromium\/(\d+)/
and /Safari\/(\d+)/ intact.
- Around line 94-106: The effect must avoid updating state or attaching
listeners after unmount: introduce a cancellation flag (e.g., let cancelled =
false) before calling nav.getBattery(), then in the promise handlers check the
flag and return early if cancelled; only call update(), setUnavailable(), or add
event listeners (battery.addEventListener('levelchange'/'chargingchange',
update)) when cancelled is false; in the cleanup set cancelled = true and still
remove listeners from battery if it was set (and only if the listeners were
previously attached) to prevent leaks — reference nav.getBattery(), the battery
variable, update(), and setUnavailable() when applying these guards.

In `@app/components/apps/music.tsx`:
- Around line 257-301: The issue is duplicate in-flight fetches causing race
updates; fix by reusing the shared in-flight promise (currently
_prefetchPromise) or add a new module-level _inflightPromise and make fetch
calls single-flight: update fetchSpotify/prefetchSpotify or have load() check
for and await the existing _inflightPromise instead of starting a new fetch,
ensure any caller (prefetchSpotify and load in MusicApp) sets and clears that
promise around the fetch so only one network request runs at a time, and replace
the setInterval polling with completion-driven polling (schedule next fetch with
setTimeout after the current one finishes) so polls never start while a previous
request is inflight; update references to _prefetchPromise, fetchSpotify, load,
_cachedData, and _cacheTime accordingly.

In `@app/components/apps/note-pad.tsx`:
- Around line 128-132: Remove the side-effectful state updater: the
setPages((prev) => { ... }) wrapper should not call setCurrentPage; instead call
e.preventDefault() and then invoke setCurrentPage directly (using the functional
updater form to compute the new page with Math.min(currentPagesLength - 1, p +
1)), and leave setPages unchanged since it isn't modifying pages. Ensure you
reference setPages and setCurrentPage by name so the change moves the navigation
logic outside the setPages updater.

In `@app/components/apps/scrapbook.tsx`:
- Around line 141-152: The global keydown handler is active even when the
scrapbook isn't focused; create a ref (e.g., scrapbookRef) on the component root
and update the useEffect/onKeyDown to only handle ArrowLeft/ArrowRight when
viewMode === 'article' AND the scrapbook has focus (check document.hasFocus()
and scrapbookRef.current.contains(document.activeElement) or similar); keep
setPage/total logic but early-return otherwise, and keep the existing
addEventListener/removeEventListener cleanup.

In `@app/components/apps/world-map.tsx`:
- Line 8: The component hardcodes GEO_URL to a CDN which can fail; instead
vendor the topology locally by adding a build step that copies
node_modules/world-atlas/countries-110m.json into public/data/world-atlas/
(create that directory), then update the GEO_URL constant in
app/components/apps/world-map.tsx to point to the local path
(/data/world-atlas/countries-110m.json); alternatively implement an API route
that reads the file from node_modules and serves it, and change GEO_URL to that
route—refer to the GEO_URL constant in world-map.tsx and the build/copy script
or API route when making the change.

In `@DESKTOP-PLAN.md`:
- Around line 55-83: The markdown has lint violations from missing blank lines
around headings and fenced code blocks and from unnamed fenced blocks; fix by
adding a blank line before and after each heading and each fenced code block and
by adding language tags to all fenced blocks (e.g., change ``` to ```typescript
for the Window Manager (React Context) block that contains types like Rect,
WindowState, and WindowManagerContextType), and apply the same fixes to the
other referenced sections (lines ~147-165, ~197-219, ~325-388) so all fenced
examples and headings comply with MD022/MD031/MD040.
- Around line 239-319: The documented "Size" entries in DESKTOP-PLAN.md are out
of sync with the actual defaults used by the running app registry (e.g., the
Music app's documented clamp sizes don't match the values defined for the
"Music" entry in app-registry.tsx); update DESKTOP-PLAN.md so each app's Size
lines exactly reflect the corresponding defaults in
app/components/desktop/app-registry.tsx (verify the "Music" app and any others
that differ), keeping the app names (e.g., "Music — live Spotify now-playing",
"Finder — 'Documents'", "Curly Browser", etc.) as anchors so future edits match
the canonical values in app-registry.tsx.

In `@scripts/spotify-oauth.mjs`:
- Around line 136-140: When the callback handler sees no code (the !code
branch), the response is ended but the HTTP callback server is left running;
update that branch in scripts/spotify-oauth.mjs to close the server after
sending the 400 response by invoking the server's close() method (e.g.,
server.close() or the exact server variable used where http.createServer(...) is
assigned), then return; locate the server created earlier (the variable assigned
from createServer/createListener) and call its close() immediately after res.end
to ensure the process can exit cleanly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 389a7c3e-27eb-4344-88af-5e8e157c1767

📥 Commits

Reviewing files that changed from the base of the PR and between 32cb24a and f0e07f5.

⛔ Files ignored due to path filters (17)
  • bun.lock is excluded by !**/*.lock
  • public/app-icons/browser.svg is excluded by !**/*.svg
  • public/app-icons/calculator.svg is excluded by !**/*.svg
  • public/app-icons/control-panel.svg is excluded by !**/*.svg
  • public/app-icons/finder.svg is excluded by !**/*.svg
  • public/app-icons/folder.svg is excluded by !**/*.svg
  • public/app-icons/journal.svg is excluded by !**/*.svg
  • public/app-icons/music.svg is excluded by !**/*.svg
  • public/app-icons/notepad.svg is excluded by !**/*.svg
  • public/app-icons/trash.svg is excluded by !**/*.svg
  • public/app-icons/world-map.svg is excluded by !**/*.svg
  • public/apple-icon.svg is excluded by !**/*.svg
  • public/browser-icons/back.svg is excluded by !**/*.svg
  • public/browser-icons/forward.svg is excluded by !**/*.svg
  • public/browser-icons/home.svg is excluded by !**/*.svg
  • public/cv/Raj_Dholakia_Resume_FullStack.pdf is excluded by !**/*.pdf
  • public/worldmap.png is excluded by !**/*.png
📒 Files selected for processing (44)
  • .gitignore
  • CLAUDE.md
  • CONTENT-ARCHIVE.md
  • DESKTOP-PLAN.md
  • MOBILE-PLAN.md
  • README.md
  • app/api/spotify/now-playing/route.ts
  • app/components/apps/calculator.tsx
  • app/components/apps/control-panel.tsx
  • app/components/apps/curly-browser.tsx
  • app/components/apps/finder.tsx
  • app/components/apps/music.tsx
  • app/components/apps/note-pad.tsx
  • app/components/apps/scrapbook.tsx
  • app/components/apps/world-map.tsx
  • app/components/boot-screen.tsx
  • app/components/crt-screen.tsx
  • app/components/desktop/app-registry.tsx
  • app/components/desktop/desktop-icon.tsx
  • app/components/desktop/desktop.tsx
  • app/components/desktop/maximize-nudge.tsx
  • app/components/desktop/window-manager.tsx
  • app/components/desktop/window.tsx
  • app/components/imac-frame.tsx
  • app/components/menu-bar.tsx
  • app/components/types.ts
  • app/components/ui/context-menu.tsx
  • app/components/welcome-screen.tsx
  • app/global.css
  • app/layout.tsx
  • app/lib/use-reduced-motion.ts
  • app/lib/utils.ts
  • app/not-found.tsx
  • app/page.tsx
  • app/robots.ts
  • app/sitemap.ts
  • components.json
  • lighthouse-report-23-12.json
  • next.config.ts
  • package.json
  • planning-details.md
  • scripts/spotify-oauth.mjs
  • types/mdx-raw.d.ts
  • wrangler.toml
💤 Files with no reviewable changes (5)
  • types/mdx-raw.d.ts
  • app/lib/use-reduced-motion.ts
  • app/lib/utils.ts
  • wrangler.toml
  • planning-details.md

Comment on lines +106 to +164
export async function GET() {
const clientId = process.env.SPOTIFY_CLIENT_ID
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN

if (!clientId || !clientSecret || !refreshToken) {
return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null })
}

// 1. Exchange refresh token for access token
const tokenRes = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`),
},
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
})

if (!tokenRes.ok) return json({ error: 'Spotify token refresh failed', isPlaying: false, track: null })

const { access_token } = await tokenRes.json() as { access_token: string }

// 2. Fetch currently playing
const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))

// Fetch top genres in parallel with the currently-playing check
const topPromise = fetchTopData(access_token)

// 204 = nothing playing, 202 = processing
if (nowRes.status === 204 || nowRes.status === 202) {
const recentRes = await fetch('https://api.spotify.com/v1/me/player/recently-played?limit=1', bearerOpts(access_token))
let lastTrack: any = null
if (recentRes.ok) {
const data = await recentRes.json() as any
const first = (data.items ?? [])[0]
if (first?.track) lastTrack = makeTrack(first.track)
}
return json({ isPlaying: false, track: lastTrack, ...await topPromise })
}

if (!nowRes.ok) {
const top = await topPromise
return json({ error: 'Spotify currently-playing failed', isPlaying: false, track: null, ...top })
}

const nowData = await nowRes.json() as any
const item = nowData?.item
if (!item) {
const top2 = await topPromise
return json({ isPlaying: false, track: null, ...top2 })
}

return json({
isPlaying: nowData.is_playing === true,
track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }),
...await topPromise,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize thrown Spotify failures to the same JSON contract.

Right now only non-OK responses are handled. If the token refresh, currently-playing, recently-played, or either top-data fetch rejects, the exception skips json() entirely and the client gets a generic 500 instead of the stable offline payload it expects. Adding a top-level try/catch and per-request timeouts keeps transient Spotify/network failures from turning into hung or shape-breaking responses.

🛡️ Suggested hardening
-function json(body: unknown) {
-  return Response.json(body, { headers: NO_CACHE_HEADERS })
+function json(body: unknown, init?: ResponseInit) {
+  return Response.json(body, {
+    ...init,
+    headers: { ...NO_CACHE_HEADERS, ...(init?.headers ?? {}) },
+  })
 }
@@
 export async function GET() {
-  const clientId = process.env.SPOTIFY_CLIENT_ID
-  const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
-  const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN
+  try {
+    const clientId = process.env.SPOTIFY_CLIENT_ID
+    const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
+    const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN
 
-  if (!clientId || !clientSecret || !refreshToken) {
-    return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null })
-  }
+    if (!clientId || !clientSecret || !refreshToken) {
+      return json({ error: 'Spotify env vars not configured', isPlaying: false, track: null, genres: [], artists: [] }, { status: 500 })
+    }
 
-  const tokenRes = await fetch('https://accounts.spotify.com/api/token', {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/x-www-form-urlencoded',
-      'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`),
-    },
-    body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
-  })
+    const tokenRes = await fetch('https://accounts.spotify.com/api/token', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded',
+        'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`),
+      },
+      body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
+      signal: AbortSignal.timeout(5000),
+    })
@@
-  return json({
-    isPlaying: nowData.is_playing === true,
-    track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }),
-    ...await topPromise,
-  })
+    return json({
+      isPlaying: nowData.is_playing === true,
+      track: makeTrack(item, { progressMs: nowData.progress_ms ?? 0 }),
+      ...await topPromise,
+    })
+  } catch {
+    return json(
+      { error: 'Spotify request failed', isPlaying: false, track: null, genres: [], artists: [] },
+      { status: 502 },
+    )
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/spotify/now-playing/route.ts` around lines 106 - 164, The GET handler
can throw if any Spotify fetch (token exchange, currently-playing,
recently-played or fetchTopData) rejects, producing a 500 and breaking the
expected JSON shape; wrap the entire function body in a try/catch and normalize
all error paths to return the same JSON contract (error, isPlaying:false,
track:null or lastTrack, plus top-data defaults). Add per-request timeouts (use
AbortController or a timeout wrapper) for the token fetch, currently-playing
fetch, recently-played fetch and the parallel fetchTopData call (reference
bearerOpts, fetchTopData, tokenRes/nowRes/recentRes, makeTrack) and ensure you
catch/recover from fetchTopData rejection (e.g., await topPromise inside try
with a safe fallback) so transient network/Spotify errors return the stable
offline payload instead of throwing.

Comment on lines +129 to +133
// 2. Fetch currently playing
const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))

// Fetch top genres in parallel with the currently-playing check
const topPromise = fetchTopData(access_token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Start fetchTopData() before awaiting the currently-playing request.

The comment says these calls run in parallel, but Line 130 fully awaits currently-playing before Line 133 even starts the top-data fetches. Since this route is polled every 10 seconds, that extra serial RTT is paid on every refresh.

♻️ Minimal fix
-  const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
-
-  // Fetch top genres in parallel with the currently-playing check
-  const topPromise = fetchTopData(access_token)
+  const nowPromise = fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
+  const topPromise = fetchTopData(access_token)
+  const nowRes = await nowPromise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 2. Fetch currently playing
const nowRes = await fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
// Fetch top genres in parallel with the currently-playing check
const topPromise = fetchTopData(access_token)
// 2. Fetch currently playing
const nowPromise = fetch('https://api.spotify.com/v1/me/player/currently-playing', bearerOpts(access_token))
const topPromise = fetchTopData(access_token)
const nowRes = await nowPromise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/spotify/now-playing/route.ts` around lines 129 - 133, Start the
fetchTopData work before awaiting the Spotify currently-playing fetch to run
them in parallel: call fetchTopData(access_token) and store the promise in
topPromise prior to awaiting the currently-playing request (nowRes) so both
network calls overlap, then await/consume topPromise as needed (e.g., await
topPromise or use Promise.all with the currently-playing response) when you need
the results; locate the code around the nowRes variable and the fetchTopData
call to make this change.

Comment on lines +104 to +116
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
const k = e.key
if (k >= '0' && k <= '9') handleDigit(k)
else if (k === '.') handleDecimal()
else if (k === '+' || k === '-' || k === '*') handleOperator(k)
else if (k === '/') { e.preventDefault(); handleOperator('/') }
else if (k === '=' || k === 'Enter') handleEquals()
else if (k === 'Escape' || k === 'c' || k === 'C') handleClear()
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [handleDigit, handleDecimal, handleOperator, handleEquals, handleClear])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope keyboard handling to the active calculator window.

Line 114 attaches a global window listener with no focus/active-window guard, so keystrokes can mutate calculator state even when another app/window is being used.

Suggested fix
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useRef } from 'react'
@@
 export function CalculatorApp() {
   const [state, setState] = useState<CalcState>(initialState)
+  const rootRef = useRef<HTMLDivElement>(null)
@@
   useEffect(() => {
     const onKeyDown = (e: KeyboardEvent) => {
+      if (!rootRef.current?.contains(document.activeElement)) return
+      if (e.metaKey || e.ctrlKey || e.altKey) return
       const k = e.key
@@
-  return (
-    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: '#fff', overflow: 'hidden' }}>
+  return (
+    <div
+      ref={rootRef}
+      tabIndex={0}
+      onMouseDown={() => rootRef.current?.focus()}
+      style={{ flex: 1, display: 'flex', flexDirection: 'column', background: '#fff', overflow: 'hidden' }}
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/apps/calculator.tsx` around lines 104 - 116, The global
keyboard handler in useEffect (onKeyDown) must ignore keystrokes when the
calculator is not active; modify onKeyDown to early-return unless the event
target is inside the calculator or the calculator has focus by using a container
ref (e.g., add a React ref like containerRef and attach it to the root element)
and checking containerRef.current?.contains(e.target as Node) (or use a
prop/state like isActive if present), then only call
handleDigit/handleDecimal/handleOperator/handleEquals/handleClear when that
check passes; keep the existing addEventListener/removeEventListener but update
the dependency list to include the new ref or isActive if needed.

Comment on lines +56 to +60
if (/Chrome\/(\d+)/.test(ua) && !/Chromium/.test(ua)) return `Chrome ${ua.match(/Chrome\/(\d+)/)?.[1] ?? ''}`
if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${ua.match(/Firefox\/(\d+)/)?.[1] ?? ''}`
if (/Safari\/(\d+)/.test(ua) && /Version\/(\d+)/.test(ua)) return `Safari ${ua.match(/Version\/(\d+)/)?.[1] ?? ''}`
if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}`
if (/Chromium\/(\d+)/.test(ua)) return `Chromium ${ua.match(/Chromium\/(\d+)/)?.[1] ?? ''}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Opera user agents are detected as Chrome due branch order.

Line 56 checks Chrome/... before Line 59 checks OPR/..., so Opera is reported as Chrome.

Suggested fix
 function detectBrowser(ua: string): string {
   if (/Edg\/(\d+)/.test(ua)) return `Edge ${ua.match(/Edg\/(\d+)/)?.[1] ?? ''}`
+  if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}`
   if (/Chrome\/(\d+)/.test(ua) && !/Chromium/.test(ua)) return `Chrome ${ua.match(/Chrome\/(\d+)/)?.[1] ?? ''}`
   if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${ua.match(/Firefox\/(\d+)/)?.[1] ?? ''}`
   if (/Safari\/(\d+)/.test(ua) && /Version\/(\d+)/.test(ua)) return `Safari ${ua.match(/Version\/(\d+)/)?.[1] ?? ''}`
-  if (/OPR\/(\d+)/.test(ua)) return `Opera ${ua.match(/OPR\/(\d+)/)?.[1] ?? ''}`
   if (/Chromium\/(\d+)/.test(ua)) return `Chromium ${ua.match(/Chromium\/(\d+)/)?.[1] ?? ''}`
   return 'Unknown'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/apps/control-panel.tsx` around lines 56 - 60, The UA detection
branch orders cause Opera (OPR/...) to match the Chrome (/Chrome\/(\d+)/) case
first; move the /OPR\/(\d+)/ check before the /Chrome\/(\d+)/ check (or alter
the Chrome pattern to explicitly exclude OPR/Chromium) so Opera returns `Opera
${...}`; update the corresponding match extraction calls (the existing ua.match
usages) accordingly to use the correct branch for `OPR\/(\d+)` and keep the
other checks for /Chromium\/(\d+)/ and /Safari\/(\d+)/ intact.

Comment on lines +94 to +106
nav.getBattery().then((b) => {
battery = b
update()
b.addEventListener('levelchange', update)
b.addEventListener('chargingchange', update)
}).catch(() => setUnavailable(true))

return () => {
if (!battery) return
battery.removeEventListener('levelchange', update)
battery.removeEventListener('chargingchange', update)
}
}, [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify async cleanup pattern around navigator.getBattery in ControlPanelApp
rg -n -C4 "getBattery\\(\\)\\.then|let battery|return \\(\\) =>" app/components/apps/control-panel.tsx

Repository: radroid/curly-cloud

Length of output: 1009


Guard getBattery() resolution after unmount to prevent state updates and listener leaks.

If unmount happens before navigator.getBattery() resolves, the promise's .then() callback will still execute, attaching listeners and calling setBatteryState after unmount. Similarly, the .catch() handler may call setUnavailable post-unmount. Add a cancellation flag to prevent all these operations if the effect cleans up first.

Suggested fix
  useEffect(() => {
    const nav = navigator as NavWithBattery
    if (!nav.getBattery) { setUnavailable(true); return }

    let battery: BatteryManager | null = null
+   let cancelled = false
    const update = () => {
-     if (battery) setBatteryState({ level: battery.level, charging: battery.charging })
+     if (!cancelled && battery) setBatteryState({ level: battery.level, charging: battery.charging })
    }

    nav.getBattery().then((b) => {
+     if (cancelled) return
      battery = b
      update()
      b.addEventListener('levelchange', update)
      b.addEventListener('chargingchange', update)
-   }).catch(() => setUnavailable(true))
+   }).catch(() => {
+     if (!cancelled) setUnavailable(true)
+   })

    return () => {
+     cancelled = true
      if (!battery) return
      battery.removeEventListener('levelchange', update)
      battery.removeEventListener('chargingchange', update)
    }
  }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/apps/control-panel.tsx` around lines 94 - 106, The effect must
avoid updating state or attaching listeners after unmount: introduce a
cancellation flag (e.g., let cancelled = false) before calling nav.getBattery(),
then in the promise handlers check the flag and return early if cancelled; only
call update(), setUnavailable(), or add event listeners
(battery.addEventListener('levelchange'/'chargingchange', update)) when
cancelled is false; in the cleanup set cancelled = true and still remove
listeners from battery if it was set (and only if the listeners were previously
attached) to prevent leaks — reference nav.getBattery(), the battery variable,
update(), and setUnavailable() when applying these guards.

Comment on lines +141 to +152
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (viewMode !== 'article') return
if (e.key === 'ArrowLeft') {
setPage((p) => Math.max(0, p - 1))
} else if (e.key === 'ArrowRight') {
setPage((p) => Math.min(total - 1, p + 1))
}
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [total, viewMode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bind article hotkeys to focused scrapbook context only.

Line 150 registers a global listener; when this app is open in article mode, ArrowLeft/ArrowRight can change pages even if another window is active.

Suggested fix
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
@@
 export function ScrapbookApp() {
+  const rootRef = useRef<HTMLDivElement>(null)
   const [page, setPage] = useState(0)
@@
   useEffect(() => {
+    if (viewMode !== 'article') return
     const onKeyDown = (e: KeyboardEvent) => {
-      if (viewMode !== 'article') return
+      if (!rootRef.current?.contains(document.activeElement)) return
       if (e.key === 'ArrowLeft') {
         setPage((p) => Math.max(0, p - 1))
       } else if (e.key === 'ArrowRight') {
         setPage((p) => Math.min(total - 1, p + 1))
       }
@@
-  return (
-    <div
+  return (
+    <div
+      ref={rootRef}
+      tabIndex={0}
+      onMouseDown={() => rootRef.current?.focus()}
       style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/apps/scrapbook.tsx` around lines 141 - 152, The global keydown
handler is active even when the scrapbook isn't focused; create a ref (e.g.,
scrapbookRef) on the component root and update the useEffect/onKeyDown to only
handle ArrowLeft/ArrowRight when viewMode === 'article' AND the scrapbook has
focus (check document.hasFocus() and
scrapbookRef.current.contains(document.activeElement) or similar); keep
setPage/total logic but early-return otherwise, and keep the existing
addEventListener/removeEventListener cleanup.


// ── Data ──────────────────────────────────────────────────────────────────────

const GEO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current geography source and check for local vendored topology asset
rg -n "GEO_URL|Geographies geography" app/components/apps/world-map.tsx
fd -i "countries-110m.json"

Repository: radroid/curly-cloud

Length of output: 191


🏁 Script executed:

cat -n app/components/apps/world-map.tsx

Repository: radroid/curly-cloud

Length of output: 5959


🏁 Script executed:

# Search for error handling, try-catch, or fallback mechanisms in the file
rg -i "error|catch|fallback|optional" app/components/apps/world-map.tsx -A 2

Repository: radroid/curly-cloud

Length of output: 45


🏁 Script executed:

# Check if there are any other geography/topology data sources or build-time data
fd -i "atlas|geography|topology|geo" --type f

Repository: radroid/curly-cloud

Length of output: 45


🏁 Script executed:

# Search for Error Boundary or error handling patterns in parent components
fd -e tsx -e ts app/components/ | head -20 | xargs rg -l "Error|Suspense|ErrorBoundary" 2>/dev/null | head -10

Repository: radroid/curly-cloud

Length of output: 580


🏁 Script executed:

# Check public directory for any vendored geography data
find public -type f -name "*.json" 2>/dev/null | head -20

Repository: radroid/curly-cloud

Length of output: 45


🏁 Script executed:

# Search for any build scripts or deployment configs that might handle this
fd -e json -e js -e mjs package.json tsconfig.json next.config.js next.config.mjs 2>/dev/null | xargs cat 2>/dev/null | grep -i "public\|public\|static\|build" | head -15

Repository: radroid/curly-cloud

Length of output: 45


🏁 Script executed:

# Search across the codebase for any other references to world-atlas or GEO_URL
rg "world-atlas|GEO_URL" --type ts --type tsx

Repository: radroid/curly-cloud

Length of output: 91


🏁 Script executed:

# Search for GEO_URL and world-atlas references with correct syntax
rg "world-atlas|GEO_URL" -i

Repository: radroid/curly-cloud

Length of output: 658


🏁 Script executed:

# Check if public directory has any assets
ls -la public/ 2>/dev/null || echo "No public directory"

Repository: radroid/curly-cloud

Length of output: 889


🏁 Script executed:

# Find where WorldMapApp is imported/used
rg "WorldMapApp" -i

Repository: radroid/curly-cloud

Length of output: 285


🏁 Script executed:

# Check for Error Boundary components in the codebase
rg "ErrorBoundary|error.*boundary" -i --max-count=10

Repository: radroid/curly-cloud

Length of output: 45


🏁 Script executed:

# Check if world-atlas has the file accessible in node_modules
find node_modules/world-atlas -name "countries-110m.json" 2>/dev/null || echo "Not found in node_modules"

Repository: radroid/curly-cloud

Length of output: 89


🏁 Script executed:

# Check Next.js or build configuration
cat next.config.mjs next.config.js 2>/dev/null || echo "No Next.js config found"

Repository: radroid/curly-cloud

Length of output: 87


🏁 Script executed:

# Check if there's a build or copy step in package.json
cat package.json | grep -A 5 '"scripts"'

Repository: radroid/curly-cloud

Length of output: 334


🏁 Script executed:

# Search for any import or require of world-atlas in the codebase
rg "import.*world-atlas|require.*world-atlas"

Repository: radroid/curly-cloud

Length of output: 45


Vendor map topology locally to prevent CDN outages.

Line 8 hardcodes a third-party CDN URL with no local fallback; if unavailable, the map renders nothing. The world-atlas package is already a dependency but unused. Either vendor the file in public/data/ with a build step to copy from node_modules, or load it via an API route. The suggested path below requires creating the directory and implementing the copy mechanism during build.

Suggested approach
  1. Add a build step to copy node_modules/world-atlas/countries-110m.json to public/data/world-atlas/
  2. Update the URL reference:
-const GEO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'
+const GEO_URL = '/data/world-atlas/countries-110m.json'

Alternatively, import the file from node_modules at runtime via an API route instead of using a CDN.

Also applies to: 86-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/apps/world-map.tsx` at line 8, The component hardcodes GEO_URL
to a CDN which can fail; instead vendor the topology locally by adding a build
step that copies node_modules/world-atlas/countries-110m.json into
public/data/world-atlas/ (create that directory), then update the GEO_URL
constant in app/components/apps/world-map.tsx to point to the local path
(/data/world-atlas/countries-110m.json); alternatively implement an API route
that reads the file from node_modules and serves it, and change GEO_URL to that
route—refer to the GEO_URL constant in world-map.tsx and the build/copy script
or API route when making the change.

Comment thread DESKTOP-PLAN.md
Comment on lines +55 to +83
### Desktop Shell
- Replaces `WelcomeScreen` on desktop viewports after boot
- Crosshatch/dithered gray background (Mac OS 1984 pattern, already in `global.css`)
- Grid of app icons (classic Finder-style)
- Decorative Trash icon bottom-right
- Menu bar at top — dynamic per focused app

### Window Manager (React Context)
```typescript
type Rect = { x: number; y: number; width: number; height: number } // px relative to CRT screen (the drag container)

type WindowState = {
appId: string
position: { x: number; y: number } // percentage of container (0–1), e.g. { x: 0.15, y: 0.20 }
zIndex: number // contiguous 0..n across open windows (not a counter)
isOpen: boolean
fromOrigin?: Rect // clicked-icon rect (CRT-relative px) for zoom-from-origin animation; cleared after open animation completes
}

type WindowManagerContextType = {
windows: Record<string, WindowState>
selectedIconId: string | null // currently highlighted desktop icon (single-click selection)
openApp: (appId: string, fromOrigin?: Rect) => void // cascaded position; fromOrigin enables zoom-in animation
closeApp: (appId: string) => void // unmounts the app; re-normalizes zIndex to keep remaining windows contiguous
focusApp: (appId: string) => void // reshuffles zIndex so target becomes top of stack (see below)
selectIcon: (appId: string | null) => void // single-click highlight
moveWindow: (appId: string, pos: { x: number; y: number }) => void // pos in percentage (0–1)
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the markdownlint violations before this becomes the canonical plan.

This file still has repeated MD022/MD031/MD040 warnings: headings and fenced blocks are missing surrounding blank lines, and several fenced blocks have no language tag. On a long-lived reference doc, leaving lint noisy either breaks docs checks or trains future edits to ignore real warnings.

Also applies to: 147-165, 197-219, 325-388

🧰 Tools
🪛 LanguageTool

[uncategorized] ~57-~57: The operating system from Apple is written “macOS”
Context: ... - Crosshatch/dithered gray background (Mac OS 1984 pattern, already in global.css) ...

(MAC_OS)

🪛 markdownlint-cli2 (0.22.1)

[warning] 55-55: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 62-62: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 63-63: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DESKTOP-PLAN.md` around lines 55 - 83, The markdown has lint violations from
missing blank lines around headings and fenced code blocks and from unnamed
fenced blocks; fix by adding a blank line before and after each heading and each
fenced code block and by adding language tags to all fenced blocks (e.g., change
``` to ```typescript for the Window Manager (React Context) block that contains
types like Rect, WindowState, and WindowManagerContextType), and apply the same
fixes to the other referenced sections (lines ~147-165, ~197-219, ~325-388) so
all fenced examples and headings comply with MD022/MD031/MD040.

Comment thread DESKTOP-PLAN.md
Comment on lines +239 to +319
### App 1: Web Browser — "Curly Browser"
- **Purpose**: Bookmarks launcher disguised as a Mac OS 1 browser
- **Design**: Mac OS 1 window chrome with **disabled** back/forward buttons, decorative read-only address bar, info banner, and a bookmarks "home page" as the only view. No internal navigation.
- **Key behavior**: Clicking any bookmark calls `window.open(url, '_blank', 'noopener,noreferrer')` — opens in the user's real browser, new tab. Right-click uses the existing Shadcn ContextMenu for "Open in New Tab" and "Copy Link" (actually copies via `navigator.clipboard`).
- **Info banner** (styled tan/yellow, top of content area): `"Curly Browser uses iframes, which most sites block for security. Links open in your real browser instead."` — this explains the quirk instead of hiding it.
- **Bookmarks** (11 total, grouped visually into "Projects" and "Tools"):
- **Projects (8)**: Penguin Mail (penguinmail.app), ARK Experience (funwithark.ca), Bridger (bridger.atawalk.ca), Stella 56 Diamonds (stella56diamonds.com), Playground (playground.createplus.club), Couples Budget (couplesbudget.ca), 75 Creates (75.createplus.club), KayVee Gems (kayveegems.com)
- **Tools (3)**: Google, Claude (claude.ai), ChatGPT (chatgpt.com)
- **Favicons**: fetched via `https://www.google.com/s2/favicons?domain=<host>&sz=128`, rendered with `imageRendering: pixelated` for lo-fi vibe.
- **Size**: `clamp(300px, 78cqw, 600px)` × `clamp(250px, 75cqh, 450px)` (large)
- **Does not iframe anything.** Don't try. The whole point is that it doesn't.

### App 2: Note Pad
- **Purpose**: Fun interactive element
- **Design**: Mac OS 1 Note Pad (simple lined text area, torn-paper top edge)
- **Features**: Single pad, localStorage save, pre-populated welcome message
- **Menu**: File > Clear Note
- **Size**: `clamp(180px, 40cqw, 300px)` × `clamp(220px, 60cqh, 380px)` (medium)

### App 3: System — "Control Panel"
- **Purpose**: Visitor diagnostics + Raj's info
- **Design**: Control Panel style (dark panels, icons) from reference image 2
- **Features**: Browser, OS, screen res, timezone, connection, language; Raj's name, role, links
- **Size**: `clamp(250px, 55cqw, 420px)` × `clamp(200px, 55cqh, 350px)` (medium)

### App 4: Calculator
- **Purpose**: Functional easter egg
- **Design**: Exact Mac OS 1 calculator (reference image 2)
- **Features**: C, E, =, *, 0-9, +, -, /, . — basic arithmetic
- **Size**: `clamp(120px, 25cqw, 200px)` × `clamp(180px, 45cqh, 280px)` (small)

### App 5: Finder — "Documents"
- **Purpose**: File explorer that mirrors the real `public/` folder with Mac-styled folder names for flavor
- **Design**: Classic Finder icon grid with breadcrumb path + Back button, scrollable content area, status bar footer
- **Folder rename map** (display label ← actual path):
- "Macintosh HD" — root
- "Applications" ← `public/app-icons/`
- "Documents" ← `public/cv/` (contains Resume.pdf)
- "Fonts" ← `public/fonts/`
- Loose files at root (Apple Logo.svg ← apple-icon.svg, Startup Sound.wav ← StartupMacI.wav, Avatar.webp ← raj-avatar.webp, etc.)
- **Interactions**:
- Single-click: selects the item (text-inverted label highlight, matching desktop icons)
- Double-click folder: navigate into it (path stack)
- Double-click file: preview in an **absolute-positioned overlay** within the Finder window (not a new top-level window — overlay has its own close button, ESC dismisses)
- Right-click file (Shadcn ContextMenu): Open, Download, Get Info (disabled), Rename (disabled)
- Back button: pop path stack
- **Preview overlay by file type**:
- `.svg` / `.png` / `.webp` / `.jpg` → `<img>` with object-fit contain
- `.wav` / `.mp3` → `<audio controls>`
- `.pdf` → `<iframe src={path}>` (browser native PDF viewer)
- `.woff` / `.woff2` / `.ttf` → message + sample text rendered in the font
- anything else → "Preview not available" + Download hint
- **Download**: right-click → Download triggers `<a href={path} download>.click()`
- **No cover letter** — ship with just the resume for now. Nothing to gate on.
- **Status bar**: "N items, 72K in disk, 400K available" (fake-realistic)
- **Size**: `clamp(220px, 48cqw, 380px)` × `clamp(180px, 50cqh, 320px)` (medium)
- **Menu**: uses the existing `FINDER_DEFAULT_MENUS` export from `app-registry.tsx`

### App 6: Scrapbook — "Journal"
- **Purpose**: Blog posts
- **Design**: Mac OS Scrapbook — page-by-page with arrows
- **Content**: 3 blog posts from CONTENT-ARCHIVE.md
- **Size**: `clamp(250px, 55cqw, 420px)` × `clamp(220px, 60cqh, 380px)` (medium)

### App 7: World Map
- **Purpose**: Travel showcase — 24 visited countries highlighted
- **Design**: Public-domain world map, dithered SVG pattern fill on visited countries, plain white on unvisited, black stroke throughout. Aesthetic reference: `public/worldmap.png` (a Risk-style Mac OS screenshot — style only; **no** game chrome, Player1, Done/Fortify/Cards).
- **Visited countries (24, ISO alpha-3)**:
`CAN, USA, GBR, FRA, DEU, AUT, ITA, CHE, HUN, CZE, NLD, BEL, LUX, EGY, SAU, ARE, IND, JPN, THA, MYS, SGP, LKA, MUS, NZL`
(Canada, United States, United Kingdom, France, Germany, Austria, Italy, Switzerland, Hungary, Czechia, Netherlands, Belgium, Luxembourg, Egypt, Saudi Arabia, UAE, India, Japan, Thailand, Malaysia, Singapore, Sri Lanka, Mauritius, New Zealand)
- **Features**: Hover tooltip with country name; small legend at a corner (patterned box + plain box with labels); viewBox scales map to window; visited count shown subtly ("24 / ~195 countries").
- **Map data**: `react-simple-maps` + `world-atlas` (TopoJSON, ~100KB) — installed as deps on `feat/mac-os-1984-desktop` before the app PR begins.
- **Size**: `clamp(280px, 70cqw, 540px)` × `clamp(220px, 60cqh, 380px)` (large)

### App 8: Music — live Spotify now-playing (vinyl parody of Apple Music)
- **Purpose**: Show what Raj is currently listening to on Spotify, as a playful Mac-era vinyl record
- **Design**: Spinning vinyl record (CSS rotate animation) with the current album art as the record label (circular mask), playlist list beside it showing recent tracks. Chicago font captions.
- **Live data**: Spotify Web API `me/player/currently-playing` endpoint, polled every ~10s. Uses refresh-token OAuth flow so no user login is needed by visitors — it's always Raj's account.
- **Fallback**: If nothing playing, show a featured/last-played track (or a static "Offline" vinyl).
- **Reduced-motion**: freeze the vinyl rotation when `prefers-reduced-motion: reduce`.
- **Size**: `clamp(180px, 40cqw, 300px)` × `clamp(220px, 60cqh, 380px)` (medium)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the documented app sizes in sync with app/components/desktop/app-registry.tsx.

Several Size entries here no longer match the defaults the desktop actually uses. For example, Line 319 documents Music as clamp(180px, 40cqw, 300px) × clamp(220px, 60cqh, 380px), but app/components/desktop/app-registry.tsx Lines 148-149 now use clamp(440px, 78cqw, 640px) × clamp(300px, 72cqh, 440px). Since this plan is meant to drive follow-up work, stale numbers will send later changes in the wrong direction.

Based on learnings: Reference DESKTOP-PLAN.md at the project root when creating implementation plans, working on features, or briefing subagents.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~299-~299: The operating system from Apple is written “macOS”
Context: ...- Purpose: Blog posts - Design: Mac OS Scrapbook — page-by-page with arrows - ...

(MAC_OS)


[uncategorized] ~305-~305: The operating system from Apple is written “macOS”
Context: ...ce: public/worldmap.png (a Risk-style Mac OS screenshot — style only; no game ch...

(MAC_OS)

🪛 markdownlint-cli2 (0.22.1)

[warning] 239-239: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 251-251: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 258-258: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 264-264: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 270-270: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 297-297: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 303-303: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 313-313: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DESKTOP-PLAN.md` around lines 239 - 319, The documented "Size" entries in
DESKTOP-PLAN.md are out of sync with the actual defaults used by the running app
registry (e.g., the Music app's documented clamp sizes don't match the values
defined for the "Music" entry in app-registry.tsx); update DESKTOP-PLAN.md so
each app's Size lines exactly reflect the corresponding defaults in
app/components/desktop/app-registry.tsx (verify the "Music" app and any others
that differ), keeping the app names (e.g., "Music — live Spotify now-playing",
"Finder — 'Documents'", "Curly Browser", etc.) as anchors so future edits match
the canonical values in app-registry.tsx.

Comment thread scripts/spotify-oauth.mjs
Comment on lines +136 to +140
if (!code) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end('Missing code in callback')
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the callback server when code is missing.

This branch returns a 400 but leaves the server running, so the script can hang instead of failing fast.

Proposed fix
   if (!code) {
     res.writeHead(400, { 'Content-Type': 'text/plain' })
     res.end('Missing code in callback')
+    console.error('❌ Missing code in callback')
+    server.close()
+    process.exit(1)
     return
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/spotify-oauth.mjs` around lines 136 - 140, When the callback handler
sees no code (the !code branch), the response is ended but the HTTP callback
server is left running; update that branch in scripts/spotify-oauth.mjs to close
the server after sending the 400 response by invoking the server's close()
method (e.g., server.close() or the exact server variable used where
http.createServer(...) is assigned), then return; locate the server created
earlier (the variable assigned from createServer/createListener) and call its
close() immediately after res.end to ensure the process can exit cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants