diff --git a/.gitignore b/.gitignore index 78418ab..57b662b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ next-env.d.ts # wrangler .wrangler/ +.dev.vars +.dev.vars.* github-banner.html -clash-api-player-response.json \ No newline at end of file +clash-api-player-response.json +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 60b65cb..7e1e0d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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. diff --git a/CONTENT-ARCHIVE.md b/CONTENT-ARCHIVE.md index 0e513a2..eb2a0f2 100644 --- a/CONTENT-ARCHIVE.md +++ b/CONTENT-ARCHIVE.md @@ -342,7 +342,7 @@ 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:** @@ -350,7 +350,7 @@ Source: `app/blog/utils.ts` ```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.' --- @@ -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) diff --git a/DESKTOP-PLAN.md b/DESKTOP-PLAN.md new file mode 100644 index 0000000..683c0b7 --- /dev/null +++ b/DESKTOP-PLAN.md @@ -0,0 +1,447 @@ +# Mac OS 1984 Desktop — Implementation Plan + +> Persistent plan document for the full desktop experience rebuild. +> Both human and Claude should reference this when planning or implementing. +> Mobile experience planned separately in `MOBILE-PLAN.md`. + +--- + +## Table of Contents + +1. [Vision](#vision) +2. [Design Decisions](#design-decisions) +3. [Architecture](#architecture) +4. [Applications](#applications) +5. [Git & PR Strategy](#git--pr-strategy) +6. [Inspiration & Anti-patterns from PostHog](#inspiration--anti-patterns-from-posthog) +7. [Status](#status) +8. [Reference Files](#reference-files) + +--- + +## Vision + +Transform the "coming soon" welcome screen into a fully interactive Mac OS 1984 desktop. The iMac G3 frame contains a working operating system with draggable windows, functional applications, and a dynamic menu bar. Visitors explore the portfolio by using apps — not by scrolling a page. + +**Current flow:** Boot sequence → Welcome dialog ("Welcome to Macintosh / Curly") +**Target flow:** Boot sequence → Welcome dialog ("Welcome to Macintosh / Curly") (without menu bar) → Desktop with app icons → Open/close/drag windows with real apps + +--- + +## Design Decisions + +| # | Decision | Choice | Notes | +|---|----------|--------|-------| +| 1 | Mobile experience | Keep current welcome screen on mobile. iPhone-style experience is a separate future project. | Desktop OS is desktop-only. | +| 2 | Draggable windows | Yes — drag via title bar, mouse only | No touch drag (desktop-only). No resize. | +| 3 | Window behavior | Click-to-focus, cascaded open positions, close box top-left | Mac OS 1 style. Responsive size per app. | +| 4 | Maximize mode | Button on iMac chin only (no menu bar option) | `isMaximized` state lives in `page.tsx`, passed as props. CRTScreen stays mounted; iMac frame (body/chin/stand) hides. Screen area fills viewport. | +| 5 | Maximize nudge | Subtle Mac-style dialog after first app open | "Tip: Use Full Screen for more space" | +| 6 | Menu bar | Dynamic — updates based on active/focused app | Apple icon menu (About) always left. Default = Finder menus (File/Edit/View/Special). Apps override via registry. | +| 7 | Blog posts | Scrapbook app (period-accurate Mac OS app) | 3 posts from CONTENT-ARCHIVE.md | +| 8 | World map style | Risk-style dithered map with pattern fills | Reference image is style guide only; travel data TBD — subagent asks user. | +| 9 | Extra apps | Music/interests-based (details TBD) | Build shell with mock data; subagent asks user for real content. | +| 10 | Icon art | User-provided 1-bit pixel art SVGs | User creates icons and adds to a folder. No external icon library. Match Mac OS 1 aesthetic. | +| 11 | Trash icon | Clickable — opens a placeholder window (bottom-right corner, outside the main icon grid) | Registered as a real app in the registry. Not functional beyond opening for now. | +| 12 | Window sizing | Container query units (`cqw`/`cqh`) with `clamp()` bounds | CRT screen is the CSS container. Windows scale proportionally on maximize. | +| 13 | Desktop icons | Single click selects (highlight), double-click opens | Mac OS style selection model. | +| 14 | Screen phases | `off → flicker → boot → welcome → desktop` | Welcome screen (no menu bar) shows briefly after boot, then transitions to desktop. | +| 15 | App registry | "Coming Soon" placeholder for unbuilt apps | Apps register real components when built. Registry also defines per-app menu bar actions. | + +--- + +## Architecture + +### 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 + 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) +} +``` + +**Derived focused window** — there is no stored `activeWindowId`. The focused window is always the one with the highest `zIndex`, derived via `useMemo`. This is how PostHog does it and it eliminates sync bugs where a separate focus field drifts from the stack order. + +```tsx +const activeWindowId = useMemo(() => { + return Object.values(windows).reduce( + (top, cur) => (cur.zIndex > (top?.zIndex ?? -1) ? cur : top), + null, + )?.appId ?? null +}, [windows]) +``` + +**Contiguous zIndex reshuffle** — `focusApp` does NOT increment a counter. It rewrites every window's `zIndex` so the target becomes `count - 1` and everything above it shifts down. This keeps values in `[0, n)` forever, no drift, no overflow. + +```tsx +const focusApp = (appId: string) => { + setWindows((prev) => { + const count = Object.values(prev).filter((w) => w.isOpen).length + const target = prev[appId] + if (!target) return prev + const next = { ...prev } + for (const id of Object.keys(next)) { + const w = next[id] + if (!w.isOpen) continue + next[id] = { + ...w, + zIndex: + id === appId ? count - 1 + : w.zIndex < target.zIndex ? w.zIndex + : w.zIndex - 1, + } + } + return next + }) +} +``` + +**Cascade-on-open**: first window opens at base `{ x: 0.12, y: 0.14 }`. Each subsequent open offsets `+0.03` on both axes from the current topmost window's position. If the offset would put the top-left past `{ x: 0.6, y: 0.55 }`, wrap back to the base. Keeps things readable without running offscreen. + +**Close normalizes zIndex**: `closeApp` removes the entry *and* decrements every remaining window's `zIndex` that was above the closed one, so the remaining set stays contiguous `[0, n-1)`. Without this, closing a middle window leaves a gap that breaks the derived focus logic. + +**Mount/unmount behavior**: App components mount when opened, unmount when closed. Apps that need persistence (e.g., Notepad) use `localStorage`. This keeps things simple and avoids hidden mounted components. + +**Re-render cost**: dragging updates `position` in the windows record, which re-renders every consumer of the context. For 8 windows this is fine; if it ever feels janky, wrap each `Window` in `React.memo` (compare by `appId`) and read its own slice via a selector hook rather than consuming the whole record. + +**Keep the provider focused**: PostHog's `App.tsx` grew to 2588 lines by bundling windows + settings + notifications + user + chat into one context. Our `WindowManagerProvider` should own windows only. Theme, settings, and any future cross-cutting concerns go in separate providers. + +### Window Component (Generic, Reusable) +The `Window` component is the frame that wraps every app. It provides: +- **Title bar**: app name, close box (top-left square), horizontal lines pattern +- **Drag**: mousedown on title bar → mousemove/mouseup listeners on `document` (so fast drags don't lose the cursor) → convert px deltas to percentage of container → update position. Use a **5px click-vs-drag threshold** — movement under 5px is treated as a focus click, not a drag, so a quick click on the title bar doesn't jitter the window. +- **Focus**: clicking anywhere in window brings to front (highest z-index) +- **Content slot**: `children` prop — each app renders inside this +- **Lazy content mount**: app content only renders once the entry animation completes. Prevents layout thrash during the pop-in. PostHog gates this on an `animating` flag; we can do the same or just delay-mount via `setTimeout(..., transitionDuration)`. +- **Open animation from origin**: when `fromOrigin` is set on the window state, the window scales in from the clicked icon's rect (Mac OS "zoom rect" effect). Close animates back to the same origin if still known. +- **Configurable props**: + - `title: string` — window title + - `appId: string` — used for window manager state + - `size: { width, height }` — responsive dimensions using `clamp()` with container query units (see below) + - `menuItems: MenuConfig[]` — app-specific menu bar items (passed up to menu bar via context) + - `showScrollbar?: boolean` — vertical scrollbar (Mac OS 1 style) + - `statusBar?: ReactNode` — optional bottom status bar (e.g., Finder's "X items") + +### Window Sizing & Position Strategy +The CRT screen element is a CSS `container-type: size` container. Window **sizes** use `cqw`/`cqh` (container query units) with `clamp()` for min/max bounds. Window **positions** are stored as percentages (0–1) of the container dimensions. Both scale automatically when toggling maximize mode — no recalculation needed. + +Size example: +```css +width: clamp(200px, 75cqw, 500px); /* 75% of screen width, bounded */ +height: clamp(200px, 70cqh, 400px); /* 70% of screen height, bounded */ +``` + +Position example: +```tsx +// Stored: { x: 0.15, y: 0.20 } +// Rendered: style={{ left: `${pos.x * 100}%`, top: `${pos.y * 100}%` }} +// On drag: convert px delta to % via containerRef.clientWidth/clientHeight +``` + +### App Registry +Defined in `app/components/desktop/app-registry.ts` (created in Phase 3). +```typescript +type MenuConfig = { + label: string // e.g., "File", "Edit", "Bookmarks" + items: { + label: string // e.g., "New Tab", "Save", "---" for divider + action?: () => void // callback on click + disabled?: boolean + shortcut?: string // display only, e.g., "⌘S" + }[] +} + +type AppDefinition = { + id: string + name: string + icon: React.FC // user-provided 1-bit pixel art SVG + defaultSize: { + width: string // clamp() with cqw, e.g. "clamp(200px, 75cqw, 500px)" + height: string // clamp() with cqh, e.g. "clamp(200px, 70cqh, 400px)" + } + menuItems: MenuConfig[] // app-specific menu bar overrides (merged with defaults) + component: React.FC // "Coming Soon" placeholder until app is built +} +``` + +### Menu Bar (Dynamic) +- **Apple icon menu** (always, leftmost): About This Macintosh, app list +- **Default menus** (Finder-style, shown when no app is focused or as base): File, Edit, View, Special +- **App-specific overrides**: each app defines `menuItems` in the registry that replace the defaults when that app is focused +- Menu bar reads the derived `activeWindowId` from the window manager context and looks up that app's `menuItems` in the registry; falls back to Finder defaults when no window is focused +- **Z-ordering**: menu bar sits at a higher z-index than all windows so dropdown menus cover windows (and windows cannot be dragged over the menu bar). Reserve a range — windows live in `[0, n)`, menu bar at e.g. `z-[9999]`. +- Default menu bar (with Apple icon + Finder menus) is part of the foundation PR + +### Rendering Hierarchy +``` +page.tsx (owns isMaximized, isDesktop, phase) + └── IMacG3Frame (receives isMaximized — hides body/chin/stand when true; CRTScreen always mounted) + └── CRTScreen (container-type: size — CSS container for window sizing + positioning) + └── phase === 'welcome' → WelcomeScreen (no menu bar, brief display) + └── phase === 'desktop' → + └── WindowManagerProvider + └── Desktop + ├── MenuBar (top, always visible) + ├── Icon Grid (desktop background layer) + └── Open Windows (positioned absolutely via %, z-ordered above icons) + └── Window (frame) → App Component (content) + +When maximized: + └── IMacG3Frame body/chin/stand hidden via CSS + └── CRTScreen (expands to fill viewport — container size changes, windows scale via cqw/cqh + %) + └── Desktop (same component tree, just bigger container) + +Mobile: + └── isDesktop === false → welcome phase never advances to desktop + └── WelcomeScreen stays permanently (current behavior preserved) +``` + +### Maximize Mode +- `isMaximized` state lives in `page.tsx`, passed as props to `IMacG3Frame` and `Desktop` +- CRTScreen stays mounted at all times +- Hides: iMac body, chin, stand (CSS toggle) +- CRTScreen expands to fill browser viewport +- Windows auto-scale: sizes via container query units, positions via percentage — no JS resize logic +- Menu bar spans full width +- Triggered by: chin button only (single toggle location) +- Transition: smooth (respects `prefers-reduced-motion`, see below) + +**Container sizing note**: CRTScreen currently uses `aspectRatio: '4 / 3'` and flex layout. Adding `container-type: size` requires the element to have a determinate size — flex + aspectRatio gives us that, but we need to verify the parent (`IMacG3Frame`) still gives CRTScreen concrete width/height. In maximize mode, CRTScreen fills `100vw × 100vh` — also concrete. Test both. + +**Reduced motion**: `prefers-reduced-motion` disables ALL of these — maximize transition, zoom-from-origin open animation, and close animation. Windows appear/disappear instantly in reduced-motion mode. Use the existing `app/lib/use-reduced-motion.ts` hook. + +--- + +## Applications + +### 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=&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` → `` with object-fit contain + - `.wav` / `.mp3` → `