Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
8a2bb2b
Fix inaccuracies in CLAUDE.md and add desktop/mobile implementation p…
radroid Apr 9, 2026
effc3fa
Refine DESKTOP-PLAN.md: percentage positions, lifted maximize state, …
radroid Apr 9, 2026
66ff622
Add PostHog research and apply learnings to DESKTOP-PLAN
radroid Apr 15, 2026
8a1c10e
Phase 1: maximize mode, desktop phase, and app assets
radroid Apr 15, 2026
d63b92b
Phase 2+3: window system, desktop shell, dynamic menu bar
radroid Apr 15, 2026
d4d5d86
Make Trash clickable — register as an app
radroid Apr 15, 2026
de0414d
Right-click context menus via shadcn/radix
radroid Apr 15, 2026
f0d56cf
Shrink boot icon, make the CD slot the maximize toggle
radroid Apr 15, 2026
72191ab
Merge pull request #2 from radroid/feat/pre-app-foundation
radroid Apr 15, 2026
59667ca
Refine DESKTOP-PLAN app specs + add worldmap reference
radroid Apr 15, 2026
b8a0371
Add react-simple-maps + world-atlas for World Map app
radroid Apr 15, 2026
bfb653a
Refine DESKTOP-PLAN for Phase 4: single PR, per-app commits
radroid Apr 16, 2026
9e6aa48
Add scripts/spotify-oauth.mjs for one-shot refresh-token capture
radroid Apr 16, 2026
0a73c41
App: Calculator — Mac OS 1 style functional calculator
radroid Apr 16, 2026
e96ba1f
App: Note Pad — lined paper, torn top edge, localStorage-backed
radroid Apr 16, 2026
e9399c0
App: Control Panel — user info + live system diagnostics
radroid Apr 16, 2026
ab192f0
App: Finder (Documents) — Mac-styled mirror of the public/ folder
radroid Apr 16, 2026
ed7b53e
Polish: +10% text and icon scale for readability
radroid Apr 16, 2026
766273f
App: Scrapbook (Journal) — page-by-page blog reader
radroid Apr 16, 2026
4ac81d1
Finder: use public/app-icons/folder.svg for folder icons
radroid Apr 16, 2026
91feca0
Window: inset focus-state horizontal lines, clean gap around close box
radroid Apr 16, 2026
dce50d8
Scrapbook: correct blog post year from 2025 to 2026
radroid Apr 16, 2026
73d940e
Polish: larger Control Panel avatar + tighter title-bar stripe inset
radroid Apr 16, 2026
a779f61
Update Phase 4 tracker with committed work + live-review adds
radroid Apr 16, 2026
559b245
Window: stripes reach window edges with close box sitting on them
radroid Apr 16, 2026
6133e8b
App: Curly Browser — fake Mac 1 browser with bookmarks launcher
radroid Apr 16, 2026
5534cf3
App: World Map — 24 visited countries with Mac 1 dither fill
radroid Apr 16, 2026
f8d29d9
Tracker: Browser + World Map committed, Music still pending
radroid Apr 16, 2026
c22a387
World Map: filled tooltip for visited countries
radroid Apr 16, 2026
d0da870
World Map: larger map, remove legend, switch to Equal Earth projection
radroid Apr 16, 2026
ff56cfc
World Map: flat Equirectangular projection with full Antarctica
radroid Apr 16, 2026
d8adfbf
World Map: window aspect matches the 2:1 map so Antarctica hits the b…
radroid Apr 16, 2026
ec249f0
World Map: move visited count to bottom-left + gitignore .dev.vars
radroid Apr 16, 2026
28e58dd
World Map: white background + border on the visited-count label
radroid Apr 16, 2026
6bf16c0
App: Music — live Spotify now-playing with spinning vinyl
radroid Apr 16, 2026
c360c2a
Curly Browser: SVG nav icons + embed Tools in-place, keep Projects ex…
radroid Apr 16, 2026
b09ddc4
Music: larger window + vinyl, clickable Spotify links per track
radroid Apr 16, 2026
0470449
Update Curly Browser app icon
radroid Apr 16, 2026
a1ea684
Tracker: Phase 4 complete — 26 commits on feat/desktop-apps, PR #3 open
radroid Apr 16, 2026
354a7a8
Music: defeat every cache layer so Recently Played actually refreshes
radroid Apr 16, 2026
40cfe36
World Map: 2× window size
radroid Apr 16, 2026
1a62a6c
Music: top 10 genres + top 10 artists, last 4 weeks only
radroid Apr 16, 2026
f5d1c33
Music: fix tsc errors + genre/artist overlap
radroid Apr 16, 2026
6bb4740
Tracker: Phase 4 complete — 32 commits, all 8 apps shipped
radroid Apr 16, 2026
fc65c9e
Merge pull request #3 from radroid/feat/desktop-apps
radroid Apr 16, 2026
39f1914
Add Phase 5 backlog, reorder desktop icons, normalize icon sizing
radroid Apr 16, 2026
a4837bb
Add window resize handles, per-window fullscreen, and bounds clamping
radroid Apr 16, 2026
3dbd801
Fix music app genre sorting to ensure descending order by count
radroid Apr 16, 2026
622ac2f
Add editable URL bar and history-based back/forward navigation to bro…
radroid Apr 16, 2026
5557744
Add multi-page support and clear-to-trash for notepad
radroid Apr 16, 2026
811271c
Add index page with click navigation to journal app
radroid Apr 16, 2026
c417e2d
Redesign control panel system settings as tiled dashboard
radroid Apr 16, 2026
d3f85ab
Fix finder icon grid alignment and right-click selection
radroid Apr 16, 2026
acb2139
Show full folder/file names in finder grid instead of truncating
radroid Apr 16, 2026
4391e20
Fix React style warning: replace borderColor with border shorthand in…
radroid Apr 16, 2026
690e418
Disable fullscreen button on calculator window
radroid Apr 16, 2026
e6a03a2
Clamp window position after mount so large windows stay in viewport
radroid Apr 16, 2026
fb01c67
Prefetch Spotify data on desktop load so Music app opens instantly
radroid Apr 16, 2026
abe72df
Change Spotify top genres/artists time range from 4 weeks to ~6 months
radroid Apr 16, 2026
6d81e74
Split Spotify top data: short_term genres, long_term artists
radroid Apr 16, 2026
b99126b
Limit Spotify genres to top 5, keep artists at top 10
radroid Apr 16, 2026
ffc5577
Update backlog: mark completed items, add new items from polish
radroid Apr 16, 2026
1606dca
Replace Finder preview overlay with separate draggable/resizable windows
radroid Apr 16, 2026
3939e93
Mark all backlog items complete
radroid Apr 16, 2026
3045f4f
Widen Finder grid columns so file names fit on one line
radroid Apr 16, 2026
1bb004c
Update browser URL bar when iframe navigates to a same-origin page
radroid Apr 16, 2026
6286dad
chore: remove stale lighthouse report and planning-details doc
radroid Apr 17, 2026
b4e4501
chore: consolidate wrangler config to wrangler.jsonc
radroid Apr 17, 2026
48535db
refactor: drop unused context-menu primitives
radroid Apr 17, 2026
3ef70b7
refactor: drop unused SIZING fields
radroid Apr 17, 2026
909c87e
refactor: drop unused CSS variables, keyframes, and mac-menu-bar class
radroid Apr 17, 2026
34fd09b
refactor: dedupe vinyl keyframes and fold duplicate effect cleanup
radroid Apr 17, 2026
b37990a
refactor: drop dead onPreview prop, countItems wrapper, stale comment…
radroid Apr 17, 2026
6095b44
refactor: drop unused locals and dead props flagged by noUnusedLocals
radroid Apr 17, 2026
12acb01
refactor: drop unused MenuBar fontSize/height props
radroid Apr 17, 2026
f6d6f15
refactor: dedupe world-map geography style + mouse handlers
radroid Apr 17, 2026
771b167
refactor: dedupe link badge styles in control-panel user card
radroid Apr 17, 2026
467e150
refactor: consolidate chicago font style in scrapbook
radroid Apr 17, 2026
d350152
refactor: consolidate chicago font style in note-pad
radroid Apr 17, 2026
85a2f72
refactor: minor dedup in desktop internals
radroid Apr 17, 2026
5bb1ffc
refactor: drop BootScreen.isActive — always true, guard never fires
radroid Apr 17, 2026
7c91c4e
refactor: collapse 4 tile components into SimpleTile helper
radroid Apr 17, 2026
2c6d4d4
refactor: inline WindowSize type — not used elsewhere
radroid Apr 17, 2026
636be7c
refactor: extract ellipsisLine + sectionHeader in music
radroid Apr 17, 2026
71fb3af
refactor: apply ellipsisLine to GenreList genre name
radroid Apr 17, 2026
d4a0701
refactor: drop redundant lastTx/lastTy in window drag
radroid Apr 17, 2026
c8db7dd
refactor: fold curly-browser goBack/goForward into step(delta)
radroid Apr 17, 2026
3f87ecc
chore: remove dead mdx-raw.d.ts type declaration
radroid Apr 17, 2026
26de071
refactor: array-drive toolbar IconButtons in curly-browser
radroid Apr 17, 2026
c9ffeed
docs: rewrite README to match current Mac OS 1984 site
radroid Apr 17, 2026
94b58b7
style: collapse defaultSize to single line per app
radroid Apr 17, 2026
44a2a31
refactor: remove redundant setVisible(false) before handleDismiss
radroid Apr 17, 2026
35a98b9
refactor: drop unused chargingTime/dischargingTime from battery state
radroid Apr 17, 2026
2554620
refactor: dedupe ContextGridItemProps — identical to GridItemProps
radroid Apr 17, 2026
3db2664
chore: delete dead app/lib/utils.ts
radroid Apr 17, 2026
0d6e89b
chore: drop stale shadcn aliases
radroid Apr 17, 2026
5f6f258
refactor: use shared useReducedMotion hook in VinylRecord
radroid Apr 17, 2026
5faec2f
refactor: inline baseUrl and collapse sitemap entry to one line
radroid Apr 17, 2026
7c6aec9
style: collapse single robots rule to one line
radroid Apr 17, 2026
7e4b703
refactor: extract cache() helper in next.config
radroid Apr 17, 2026
c990674
style: inline single-item File/Close menus
radroid Apr 17, 2026
8bea120
refactor: table-drive detectNetwork label mapping
radroid Apr 17, 2026
62f6712
refactor: inline single-use OpenMode type
radroid Apr 17, 2026
2a8e8b5
refactor: drop redundant width/height in IconButton style
radroid Apr 17, 2026
f2e0841
refactor: drop redundant width/height in FolderIcon style
radroid Apr 17, 2026
28d47f2
refactor: drop redundant width/height CSS in BookmarkTile + Avatar
radroid Apr 17, 2026
9d74ad5
refactor: extract makeTrack helper in spotify route
radroid Apr 17, 2026
5530280
style: collapse FileThumbnail style to one line
radroid Apr 17, 2026
a74b3c0
style: collapse EmbedView iframe style to one line
radroid Apr 17, 2026
ddba8ef
refactor: simplify text-field focus check in note-pad key handler
radroid Apr 17, 2026
a699381
style: collapse scrapbook IndexRow title/date styles to one-liners
radroid Apr 17, 2026
d4744a1
style: tighten BatteryTile bar/nub/fill styles
radroid Apr 17, 2026
7a7567c
style: collapse BookmarkTile favicon style to one line
radroid Apr 17, 2026
61d31b7
style: group maximize-nudge dialog style props
radroid Apr 17, 2026
306f36c
style: tighten Window root/title styles + transition array
radroid Apr 17, 2026
8d67873
style: tighten page.tsx outer/inner container styles
radroid Apr 17, 2026
e828390
style: fold 4 isMaximized-only props into a single spread in imac-frame
radroid Apr 17, 2026
7506538
style: tighten imac-frame chin/stand/bezel styles
radroid Apr 17, 2026
e2d04b3
style: tighten imac-frame CD-slot and restore-button styles
radroid Apr 17, 2026
3d33f64
style: group desktop-icon inline styles
radroid Apr 17, 2026
165312b
refactor: extract chromeButtonStyle, tighten Close/Fullscreen buttons
radroid Apr 17, 2026
7bb7941
style: group finder root/toolbar/grid/status-bar styles
radroid Apr 17, 2026
5170f97
style: group menu-bar styles; use Object.assign in hover handlers
radroid Apr 17, 2026
af6ef7f
style: group scrapbook root/paper/nav-button styles
radroid Apr 17, 2026
bd6b2fd
style: group scrapbook article-view + footer styles
radroid Apr 17, 2026
b2872b2
style: group control-panel style blocks
radroid Apr 17, 2026
9ad8026
style: group note-pad nav-button + root + textarea styles
radroid Apr 17, 2026
57716ed
refactor: array-drive calculator buttons
radroid Apr 17, 2026
d31d04a
refactor: extract centered + grooveRings helpers in VinylRecord
radroid Apr 17, 2026
a88ff1d
style: group curly-browser IconButton/tile/sections/embed styles
radroid Apr 17, 2026
0ade113
style: tighten music ArtistList row + ProgressBar
radroid Apr 17, 2026
aff2988
style: group music main-render styles (loading/offline/columns)
radroid Apr 17, 2026
0f02471
style: group GenreList empty-state + bar styles
radroid Apr 17, 2026
8e0939f
style: group world-map root/visited-count/tooltip styles
radroid Apr 17, 2026
eceea38
style: group window drag/resize let decls and containerRect literals
radroid Apr 17, 2026
3ab4879
style: inline window normalStyle/zoomStyle small objects
radroid Apr 17, 2026
f44500f
style: tighten window title/content/status-bar styles
radroid Apr 17, 2026
fa08b5f
style: tighten desktop root/icon-grid/trash/component props
radroid Apr 17, 2026
361b6ea
style: group context-menu style constants and one-line inset types + …
radroid Apr 17, 2026
76e5994
style: collapse layout metadata objects; extract TITLE/DESCRIPTION
radroid Apr 17, 2026
9079432
style: group not-found.tsx inline styles
radroid Apr 17, 2026
9933e8b
style: group welcome-screen inline styles
radroid Apr 17, 2026
531d038
style: group finder DocIcon/GridItem/ContextGridItem styles
radroid Apr 17, 2026
a28d723
refactor: collapse finder currentEntries + breadcrumb IIFEs
radroid Apr 17, 2026
daae79b
style: inline menu-bar divider return and AppleGlyph svg attrs
radroid Apr 17, 2026
3b8785a
refactor: tighten window-manager cascade + openWindows helpers
radroid Apr 17, 2026
4288db4
refactor: tighten window-manager openApp/closeApp/move/resize/focusRe…
radroid Apr 17, 2026
2c59ac0
refactor: d() + D helpers for disabled menu items across app-registry
radroid Apr 17, 2026
db4678b
refactor: tighten window resize onMove/onUp branches
radroid Apr 17, 2026
de072d0
style: tighten window drag onUp (inline rafId cancel + move payload)
radroid Apr 17, 2026
00b4d49
refactor: collapse note-pad clearCurrentPage branches
radroid Apr 17, 2026
58c65e8
style: tighten calculator keyboard handler + button styles + root
radroid Apr 17, 2026
6abf6ce
refactor: fold isFinite+formatResult guards in calculator handlers
radroid Apr 17, 2026
2e12c65
style: inline imac-frame hover handlers (one-line if-assignment)
radroid Apr 17, 2026
87ecfa9
style: inline calculator handleDigit/handleDecimal early returns
radroid Apr 17, 2026
f115d54
style: tighten note-pad footer buttons (inline children + style objects)
radroid Apr 17, 2026
c80f89d
style: tighten desktop-icon openFromIcon + click handlers
radroid Apr 17, 2026
58a61b3
style: group curly-browser main root + toolbar + input styles
radroid Apr 17, 2026
6455da3
style: tighten window clamp-position raf body
radroid Apr 17, 2026
29d7395
style: tighten ClockTile time/date objects + BatteryTile useEffect
radroid Apr 17, 2026
a29fa98
refactor: use try/catch/finally in music load; merge duplicated setLo…
radroid Apr 17, 2026
c0eee04
style: tighten prefetchSpotify (inline then/catch/finally bodies)
radroid Apr 17, 2026
a6ee4b5
style: tighten note-pad storage helpers (inline guards + empty catches)
radroid Apr 17, 2026
3942a24
style: inline BookmarkTile label style + context-menu item children
radroid Apr 17, 2026
1ec7acc
style: inline scrapbook footer nav button children
radroid Apr 17, 2026
cd25339
style: tighten ContextGridItem helpers (drop blank line, inline handl…
radroid Apr 17, 2026
b64e8e6
style: tighten BatteryTile cleanup + label ternary
radroid Apr 17, 2026
b64c568
style: inline world-map base geography style literal
radroid Apr 17, 2026
20c195b
style: tighten window useLayoutEffect + content-mount useEffect
radroid Apr 17, 2026
56cdecf
style: spread-merge imac-frame body isMaximized-only props
radroid Apr 17, 2026
852fd7b
style: inline page.tsx boot useEffect early-return + drop blanks
radroid Apr 17, 2026
4a8a1d7
style: inline BookmarkTile onKeyDown
radroid Apr 17, 2026
52bc5e8
style: inline finder GridItem onKeyDown
radroid Apr 17, 2026
c7b491a
style: group music ellipsisLine + sectionHeader constants
radroid Apr 17, 2026
7eb3ae1
style: inline chicago const in music
radroid Apr 17, 2026
3d9f4b4
style: inline chicago consts across all 5 other apps
radroid Apr 17, 2026
a212690
style: tighten CRTScreen styles + overlay conditional spread
radroid Apr 17, 2026
077b28f
style: tighten boot-screen useEffect + style
radroid Apr 17, 2026
a61a726
style: drop blank lines in useReducedMotion
radroid Apr 17, 2026
22a3133
style: inline fetchSpotify fetch options
radroid Apr 17, 2026
a0f8d90
style: tighten spotify route fetch options + return objects
radroid Apr 17, 2026
e19e406
style: inline fetchTopData opts construction
radroid Apr 17, 2026
fbde03e
refactor: drop unused status param in spotify json() helper
radroid Apr 17, 2026
238550c
style: inline desktop maximize ContextMenuItem child
radroid Apr 17, 2026
1c7d3f7
style: collapse note-pad handleChange useCallback wrapping
radroid Apr 17, 2026
b590d14
style: collapse SIZING entries to aligned one-liners
radroid Apr 17, 2026
83b82d7
style: collapse FILE_TREE folder headers to one line
radroid Apr 17, 2026
0fa1e94
style: tighten maximize-nudge (inline props type, button children, pi…
radroid Apr 17, 2026
e17e7fa
style: inline imac-frame transition ternaries
radroid Apr 17, 2026
e28f56b
docs: drop CLAUDE.md reference to deleted app/lib/utils.ts
radroid Apr 17, 2026
078f303
chore: remove POSTHOG-RESEARCH.md (462 lines), update DESKTOP-PLAN refs
radroid Apr 17, 2026
68ab5da
docs: trim DESKTOP-PLAN historical Progress Tracker + Readiness Check…
radroid Apr 17, 2026
7484bcb
refactor: drop unused audioRef in boot-screen, use local audio var
radroid Apr 17, 2026
1939859
refactor: inline getFilename helper + drop unreachable breadcrumb branch
radroid Apr 17, 2026
8c41e5c
refactor: move VINYL_KEYFRAMES to global.css
radroid Apr 17, 2026
68ab543
refactor: inline BookmarkTile helpers + drop redundant disabled styles
radroid Apr 17, 2026
187463f
chore: drop unused note-pad textareaRef + redundant onFocus + empty t…
radroid Apr 17, 2026
21ec008
refactor: drop makePlaceholder factory + extract bearerOpts helper
radroid Apr 17, 2026
b67e8ee
docs: remove BACKLOG.md (all 25 items complete, history in git log)
radroid Apr 17, 2026
0f44ad8
style: collapse multi-line JSX tags in world-map and not-found
radroid Apr 17, 2026
4d935c2
style: collapse window.tsx resize base styles to one-liners
radroid Apr 17, 2026
fb56a3e
style: collapse control-panel tile/link style consts to one-liners
radroid Apr 17, 2026
9257aa5
style: collapse remaining multi-line React.CSSProperties consts
radroid Apr 17, 2026
681d282
docs: trim DESKTOP-PLAN Implementation Phases + Subagent Contracts
radroid Apr 17, 2026
553b0f3
docs: trim DESKTOP-PLAN Git & PR Strategy to branch diagram + merge path
radroid Apr 17, 2026
35cc2d6
refactor: drop redundant client-side genre sort (API pre-sorts)
radroid Apr 17, 2026
a1bc0ae
style: collapse multi-line context-menu / app-registry imports to one…
radroid Apr 17, 2026
5d8d2c4
style: collapse MenuItem union + ComingSoon div style to one line
radroid Apr 17, 2026
65417f8
style: compact menu-bar — APPLE_MENU items, menuItemStyle, MenuDropdo…
radroid Apr 17, 2026
6ff5cf9
style: collapse multi-line function-component props destructures to o…
radroid Apr 17, 2026
ab6b538
style: collapse WindowProps/IconButtonProps types + merge duplicate i…
radroid Apr 17, 2026
73e891d
style: collapse imperative mouse-event handlers to one line each
radroid Apr 17, 2026
0c40299
Revert "style: collapse imperative mouse-event handlers to one line e…
radroid Apr 17, 2026
06372d9
Revert "style: collapse WindowProps/IconButtonProps types + merge dup…
radroid Apr 17, 2026
37cec0d
Revert "style: collapse multi-line function-component props destructu…
radroid Apr 17, 2026
9cb1427
Revert "style: compact menu-bar — APPLE_MENU items, menuItemStyle, Me…
radroid Apr 17, 2026
4c094f1
Revert "style: collapse MenuItem union + ComingSoon div style to one …
radroid Apr 17, 2026
5b6d52b
Revert "style: collapse multi-line context-menu / app-registry import…
radroid Apr 17, 2026
c8b7fe1
Revert "style: collapse remaining multi-line React.CSSProperties consts"
radroid Apr 17, 2026
28ee192
Revert "style: collapse control-panel tile/link style consts to one-l…
radroid Apr 17, 2026
97931b8
refactor: drop unused `export` on WindowState (only used within file)
radroid Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ next-env.d.ts

# wrangler
.wrangler/
.dev.vars
.dev.vars.*

github-banner.html
clash-api-player-response.json
clash-api-player-response.json
.claude/
16 changes: 9 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,29 @@ bun run preview # Build and preview Cloudflare deployment locally
bun run deploy # Build and deploy to Cloudflare Pages
```

### Dev server is always running
Assume `bun run dev` is already running in another terminal. Do **not** start it yourself. If you need to verify changes, ask the user to check in the browser, or use `bunx tsc --noEmit` for a type-only check.

## Architecture

This is a minimal "coming soon" portfolio site themed after **Mac OS System 1 (1984)**. It has a single page with two states:

1. **Boot Sequence** — Terminal-style loading animation (black background, white monospace text, sequential line reveals). Stored in `sessionStorage` so it only plays once per session.
1. **Boot Sequence** — CRT flicker → Happy Mac icon on dithered gray background with startup sound. Stored in `sessionStorage` so it only plays once per session.
2. **Welcome Screen** — "Welcome to Macintosh." dialog on a dithered gray desktop background with a decorative menu bar.

### Key Files
- `app/page.tsx` — Client component with `BootSequence` and `WelcomeScreen` components
- `app/page.tsx` — Client component with `BootScreen` and `WelcomeScreen` components
- `app/global.css` — Mac OS 1984 theme (Chicago font, crosshatch pattern, dialog/menu styles)
- `app/layout.tsx` — Minimal root layout with JetBrains Mono font
- `app/lib/use-reduced-motion.ts` — Accessibility hook for animation preferences
- `app/lib/utils.ts` — `cn()` utility (clsx + tailwind-merge)
- `app/not-found.tsx` — Mac-style "system error" 404 page

### Content Archive
All previous site content (projects, blog posts, timeline, skills) is preserved in `CONTENT-ARCHIVE.md` at the project root.

### Implementation Plan
The full desktop experience implementation plan is in `DESKTOP-PLAN.md` at the project root. Reference this file when creating plans, working on implementation, or briefing subagents. It contains all design decisions, architecture details, git/PR strategy, subagent contracts, and a progress tracker.

## Code Style

### Collocate Everything
Expand All @@ -47,10 +52,7 @@ This project uses **Tailwind CSS v4 alpha** with `@tailwindcss/postcss`. There i
There is only one theme. CSS variables use standard hex values (not RGB triplets). There are no `data-theme` attributes, no theme switcher, no localStorage theme state.

### Chicago Bitmap Font
The site uses ChicagoFLF (`public/fonts/ChicagoFLF.woff2`) for the Mac OS look. `--font-chicago` falls back to VT323 then monospace. Anti-aliasing is disabled (`-webkit-font-smoothing: none`) to preserve the bitmap aesthetic.

### Two Wrangler Configs
Both `wrangler.toml` and `wrangler.jsonc` exist at the root with identical settings. Edit both or consolidate.
The site uses ChicagoFLF (`public/fonts/ChicagoFLF.woff`) for the Mac OS look. `--font-chicago` falls back to VT323 then monospace. Anti-aliasing is disabled (`-webkit-font-smoothing: none`) to preserve the bitmap aesthetic.

### No Test Framework
There are no tests, no test runner, no test dependencies. Do not assume you can validate changes with automated tests.
Expand Down
6 changes: 3 additions & 3 deletions CONTENT-ARCHIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,15 +342,15 @@ Source: `app/blog/utils.ts`
### Post 1: Building Product for Humans, Not Metrics

- **slug:** `building-for-humans`
- **publishedAt:** 2025-01-15
- **publishedAt:** 2026-01-15
- **summary:** The best products measure success by whether people grew, not just whether numbers went up.

**Full MDX content:**

```mdx
---
title: 'Building Product for Humans, Not Metrics'
publishedAt: '2025-01-15'
publishedAt: '2026-01-15'
summary: 'The best products measure success by whether people grew, not just whether numbers went up.'
---

Expand Down Expand Up @@ -949,7 +949,7 @@ googleBot:
- `https://curlycloud.dev/history`

**Blog routes (from posts):**
- `https://curlycloud.dev/blog/building-for-humans` (lastModified: 2025-01-15)
- `https://curlycloud.dev/blog/building-for-humans` (lastModified: 2026-01-15)
- `https://curlycloud.dev/blog/generalism-future` (lastModified: 2025-02-10)
- `https://curlycloud.dev/blog/physical-digital-tension` (lastModified: 2025-03-05)

Expand Down
447 changes: 447 additions & 0 deletions DESKTOP-PLAN.md

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions MOBILE-PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Mobile Experience — iPhone OS 1 (2007) Style

> **Status**: Planning — to be built after desktop experience is complete.

---

## Vision

On mobile viewports, replace the current welcome screen with an iPhone OS 1 (original iPhone, 2007) experience. Same apps as desktop, but wrapped in iPhone-native chrome instead of Mac OS windows.

---

## Key Differences from Desktop

- **No draggable windows** — apps open full-screen, one at a time
- **Springboard** — home screen with rounded app icon grid (4 columns)
- **Slide to unlock** — replaces the boot sequence on mobile
- **Status bar** — top bar with carrier name, time, battery (styled, not real)
- **Home button** — tap to return to springboard
- **No menu bar** — apps have their own navigation (back button, title bar)

---

## Assets

User has assets ready: phone screen frame, logos, icons. To be shared when mobile phase begins.

---

## Shared Components

App content components (Calculator logic, Notepad text area, etc.) should be reusable between desktop and mobile. Only the shell/chrome differs:
- Desktop: `Window` component wraps app content
- Mobile: full-screen view with iPhone OS navigation wraps same app content

---

## Implementation

- Separate phase after desktop is fully merged to `main`
- Own branch: `feat/mobile-iphone-os`
- Responsive detection in `page.tsx` already exists (`isDesktop` state)
56 changes: 13 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,26 @@
# Portfolio & Blog
# curlycloud.dev

Personal portfolio and blog site showcasing my work as a generalist builder—bridging the gap between physical experiences and digital infrastructure.
A Mac OS System 1 (1984) themed "coming soon" site for [curlycloud.dev](https://curlycloud.dev).

Visit at: **https://curlycloud.dev**
## Stack

## About
- Next.js 15 (App Router) + TypeScript
- Tailwind CSS v4 alpha
- Chicago bitmap font (`public/fonts/ChicagoFLF.woff`)
- Deployed to Cloudflare Pages via OpenNext

I build experiences—both digital and physical—for people who want more from life. Engineering background. Founder at ARK Experiences. Engineer, Developer and Entrepreneur.

## Features

- **Portfolio Showcase**: Interactive carousel displaying my projects and work
- **Blog**: MDX and Markdown support for writing and sharing thoughts
- **SEO Optimized**: Complete with sitemap, robots.txt, and JSON-LD schema
- **RSS Feed**: Subscribe to stay updated with new posts
- **Dynamic OG Images**: Automatically generated Open Graph images for social sharing
- **Syntax Highlighting**: Beautiful code blocks in blog posts
- **Dark Mode**: Theme switching with smooth transitions
- **Modern Stack**: Built with Next.js 16, Tailwind v4, and TypeScript
- **Performance**: Vercel Speed Insights and Web Analytics integrated

## Tech Stack

- **Framework**: Next.js 16 (App Router)
- **Styling**: Tailwind CSS v4
- **Content**: MDX with next-mdx-remote
- **Deployment**: Cloudflare Pages (via OpenNext)
- **Fonts**: Geist Sans & Geist Mono
- **UI Components**: shadcn/ui

## Development

Install dependencies:
## Develop

```bash
bun install
bun run dev # http://localhost:3000
```

Run the development server:
## Build & deploy

```bash
bun run dev
bun run build # Next.js production build
bun run deploy # OpenNext → Cloudflare Pages
```

Build for production:

```bash
bun run build
```

Deploy to Cloudflare:

```bash
bun run deploy
```
See `CLAUDE.md` for architecture notes and `DESKTOP-PLAN.md` for the full implementation plan.
164 changes: 164 additions & 0 deletions app/api/spotify/now-playing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
export const dynamic = 'force-dynamic'
export const revalidate = 0
export const fetchCache = 'force-no-store'

// Response headers that prevent both browser and Cloudflare edge from
// caching the response. Without these, the Recently Played list can get
// stuck on a stale copy from the first request even though the client
// polls every 10s.
const NO_CACHE_HEADERS = {
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
'CDN-Cache-Control': 'no-store',
'Cloudflare-CDN-Cache-Control': 'no-store',
} as const

function json(body: unknown) {
return Response.json(body, { headers: NO_CACHE_HEADERS })
}

const bearerOpts = (token: string) =>
({ headers: { Authorization: `Bearer ${token}` }, cache: 'no-store' as const })

interface TopData {
genres: { name: string; count: number }[]
artists: { name: string; image: string | null; spotifyUrl: string | null }[]
}

function makeTrack(item: any, nowPlaying?: { progressMs: number }) {
return {
name: item.name ?? 'Unknown',
artist: (item.artists ?? []).map((a: any) => a.name).join(', '),
album: item.album?.name ?? '',
albumArt: item.album?.images?.[0]?.url ?? null,
spotifyUrl: item.external_urls?.spotify ?? null,
...(nowPlaying && { progressMs: nowPlaying.progressMs, durationMs: item.duration_ms ?? 0 }),
}
}

async function fetchTopData(accessToken: string): Promise<TopData> {
try {
const opts = bearerOpts(accessToken)
// Two separate calls: short_term for genres (recent listening), long_term for artists (all time)
const [genreRes, artistRes] = await Promise.all([
fetch('https://api.spotify.com/v1/me/top/artists?limit=50&time_range=short_term', opts),
fetch('https://api.spotify.com/v1/me/top/artists?limit=50&time_range=long_term', opts),
])

// Top 10 artists from long_term
let artists: TopData['artists'] = []
if (artistRes.ok) {
const data = await artistRes.json() as any
artists = (data.items ?? []).slice(0, 10).map((a: any) => ({
name: a.name ?? 'Unknown',
image: a.images?.[0]?.url ?? null,
spotifyUrl: a.external_urls?.spotify ?? null,
}))
}

// Top 10 genres from short_term, deduplicated
let genres: TopData['genres'] = []
if (genreRes.ok) {
const data = await genreRes.json() as any
const items: any[] = data.items ?? []

const raw = new Map<string, number>()
for (const artist of items) {
for (const genre of artist.genres ?? []) {
const key = genre.toLowerCase()
raw.set(key, (raw.get(key) ?? 0) + 1)
}
}

// Merge entries where one is the other + trailing "s"
const merged = new Map<string, number>()
const keys = Array.from(raw.keys()).sort((a, b) => (raw.get(b) ?? 0) - (raw.get(a) ?? 0))
const consumed = new Set<string>()
for (const key of keys) {
if (consumed.has(key)) continue
let count = raw.get(key) ?? 0
const singular = key.endsWith('s') ? key.slice(0, -1) : null
const plural = key + 's'
if (singular && raw.has(singular) && !consumed.has(singular)) {
count += raw.get(singular) ?? 0
consumed.add(singular)
}
if (raw.has(plural) && !consumed.has(plural)) {
count += raw.get(plural) ?? 0
consumed.add(plural)
}
consumed.add(key)
const label = key.charAt(0).toUpperCase() + key.slice(1)
merged.set(label, count)
}

genres = Array.from(merged.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([name, count]) => ({ name, count }))
}

return { genres, artists }
} catch {
return { genres: [], artists: [] }
}
}

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,
})
}
Loading