PR #3: Phase 4 apps — 7 of 8 apps + Spotify helper (Music pending) - #3
Merged
Conversation
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>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
raj-portfolio | 6bb4740 | Commit Preview URL Branch Preview URL |
Apr 16 2026, 01:37 AM |
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>
Switch from geoEqualEarth (curved edges) to geoEquirectangular — the plate carrée projection — so latitude and longitude are evenly spaced straight lines and country borders read cleanly without curvature. Scale 128 fills the 800×400 viewBox exactly (2:1 aspect matches the projection's native ratio), and center [0, 0] keeps the full latitude range from 90N to 90S in frame so Antarctica is always visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ottom The previous height left ~90px of empty space below the Equirectangular map in the default window. Recompute the height clamp from the width using the map's native 2:1 content ratio: content height = width × 0.5 (= 35cqw), plus 21px for the title bar. Max clamps at 291px (for the 540px max width), min at 161px. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Visited count label moves from top-left to bottom-left so it doesn't sit on top of Canada / the Arctic region. - Adds .dev.vars and .dev.vars.* to .gitignore so the local wrangler secrets file (mirrors Cloudflare Pages env vars for bun run preview) stays out of git. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents country borders in the map from bleeding through the text and making the count hard to read. Matches the styling pattern used elsewhere in the OS — plain white panel with a 1px black border. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the Music app and the /api/spotify/now-playing route that powers
it. Runtime architecture: server-side refresh-token grant exchanges
SPOTIFY_REFRESH_TOKEN → access_token on each invocation, then fetches
currently-playing + recently-played from the Spotify Web API. Returns
a normalized { isPlaying, track, recent } shape; every failure path
returns HTTP 200 with an error field so the UI can show an offline
vinyl instead of breaking the desktop.
UI:
- Left column: vinyl record — spinning when isPlaying, paused otherwise,
with the current album art clipped to a circle as the label. Status
pill above ("Now Playing" / "Last Played") + track/artist + a subtle
2px progress bar during active playback.
- Right column: Recently Played list with small album art thumbnails
and track/artist rows.
- Offline state: solid white "Offline" vinyl + "Not connected to
Spotify" caption, no broken UI.
- prefers-reduced-motion: vinyl freezes instantly.
Polls /api/spotify/now-playing every 10s; client-side caching disabled
via cache: 'no-store'.
Widens the Music window defaultSize from the original small/medium
clamp to clamp(320px, 60cqw, 480px) × clamp(220px, 55cqh, 340px) so
the two-column vinyl+playlist layout has room to breathe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ternal - New 16×16 1-bit SVGs for Back / Forward / Home in public/browser-icons/ replace the old Unicode glyph buttons in the toolbar. - Add per-bookmark openMode: Projects keep opening in a real browser tab (iframes would be blocked anyway); Tools (Google / Claude / ChatGPT) now embed inside the Curly Browser window via an internal view state. - Embed view renders an <iframe sandbox="..."> and a persistent yellow footer banner with an "Open in real browser →" escape button, since Google / Claude / ChatGPT all set frame-ancestors and the iframe is very likely to come up blank. - Back and Home buttons both return to the bookmarks page when in the embed view; they stay disabled on the home page. Forward remains decorative for now. - Address bar reflects the current view URL, read-only. - Right-click context menu keeps "Open in New Tab" as an always-external action regardless of the bookmark's default openMode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Window default size goes from 320-480 × 220-340 up to 440-640 × 300-440 so the vinyl + playlist layout has room to breathe. Internal content scales up with it: - Vinyl 150→210px, label 66→96px, hole 10→14px, groove rings rescaled - Left column flex 50→55%, padding 12→18px, status label 9→11px - Track title 12→15px, artist 11→13px - Recently-played header 10→12px; each row: thumb 30→44px, padding 4→7px, track name 11→13px, artist 10→11px Also threads spotifyUrl through the API route (from item.external_urls.spotify in both currently-playing and recently- played responses) and uses it to: - Wrap each row in the Recently Played list in an <a target="_blank"> that opens the track in Spotify, with a soft hover highlight - Wrap the main vinyl + album art label in the same link when a URL is present Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Small touch-up from the user-provided browser.svg asset.
All 8 apps are now on feat/desktop-apps with their iterative live-review polish commits logged. Next: review → merge PR #3 into feat/mac-os-1984-desktop, then final integration pass before merging to main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The list was getting stuck on a stale copy because the response was
being cached somewhere between Spotify and the browser — likely the
Cloudflare edge, since Spotify's recently-played endpoint itself
changes whenever new tracks play.
Three layers of no-cache:
1. Route-level: add `revalidate = 0` and `fetchCache = 'force-no-store'`
alongside the existing `dynamic = 'force-dynamic'`. Belt and braces.
2. Response headers: every Response.json now goes through a `json()`
helper that attaches `Cache-Control`, `CDN-Cache-Control`, and
`Cloudflare-CDN-Cache-Control` = `no-store`. Cloudflare's edge
respects the CDN-* headers specifically.
3. Client fetch: append `?t=${Date.now()}` to the request URL and pass
`Cache-Control: no-cache` — so even if a downstream layer keyed on
URL+Vary, every poll has a unique URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clamp bumps: width 280–540 → 560–1080, height 161–291 → 301–561. Fluid middle moves from 70cqw to 90cqw so the window fills more of the CRT screen in both normal and maximized modes. Height formula stays in sync with the 2:1 map aspect (45cqw + 21px) so Antarctica still lines up exactly with the bottom of the window. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- API route fetches /me/top/artists with time_range=short_term (~4 weeks) instead of medium_term (~6 months). - Aggregates genres from the top 50 artists and deduplicates near- duplicates (e.g. "afrobeats" vs "afrobeat" merged by stripping trailing "s" and combining counts). Returns top 10 genres + top 10 artists (name, image, spotifyUrl) alongside the currently-playing track. - Right panel now shows two scrollable sections: "Top Genres" with bar charts + "Top Artists" with circular thumbnails linked to their Spotify pages. - OAuth helper updated with user-top-read scope (user must re-run once to capture a new refresh token). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route: Array.from() on Map iterators (no downlevelIteration flag), type lastTrack as `any` so the later assignment satisfies strictNullChecks. - Component: remove flex:1 + overflowY from GenreList so both sections render at their natural height and the parent column scrolls through them. Previously GenreList's flex:1 hogged all vertical space, leaving artists barely visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Marks every Phase 4 commit as done including the live-review polish (cache-busting, world map 2× window, genres+artists panel, overlap fix). Updates the Readiness Checklist: Phase 4 section is closed, Cloudflare Pages env vars confirmed set, Phase 5 integration items are the remaining work before merging to main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
radroid
added a commit
that referenced
this pull request
Apr 17, 2026
The PR #3 workflow steps and "Why This Strategy" rationale were execution notes for completed work. Branch tree retained as the canonical merge path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4 rollout. Single branch, per-app commits. Ships 7 of 8 apps now; Music lands in a follow-up commit once the Spotify refresh token is captured.
Commits (in order)
scripts/spotify-oauth.mjs— one-shot OAuth helper for capturing the refresh token locally (never commits secrets)public/with Mac-styled folder names, preview overlay per file type, right-click Open/Downloadpublic/app-icons/folder.svgCONTENT-ARCHIVE.mdStill to land
node scripts/spotify-oauth.mjs) to captureSPOTIFY_REFRESH_TOKENinto.env.local.Test plan
Deploy notes for Spotify (when Music lands)
Set these three env vars in both Cloudflare Pages Preview and Production scopes:
SPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETSPOTIFY_REFRESH_TOKENThe backend
/api/spotify/now-playingroute uses the server-side refresh-token grant, so the same code works on localhost, every preview*.pages.devURL, and production — no redirect URI needed at runtime.🤖 Generated with Claude Code