···11-# Handoff: UI polish — feed, login, and shell
11+# Handoff: threaded reply views
2233## What this project is
4455Dudesky is a Bluesky client built with TanStack Start (React, SSR, file-based routing). It uses ATProto OAuth to authenticate users via their Bluesky handle. Stack: TanStack Start + Vite, Tailwind CSS v4, `@atproto/oauth-client-node` for OAuth, `better-sqlite3` for persistence.
6677-## What was built last session
77+## What was built in previous sessions
8899### Database-backed OAuth stores (`src/lib/db.ts`, `src/lib/oauth-client.ts`)
1010···25252626The server function returns a mapped subset of the feed data (not raw ATProto types) to avoid a type incompatibility with `createServerFn`'s serialization constraints (`{ [x: string]: unknown }` vs `{ [x: string]: {} }`).
27272828+### Feed UI polish (`src/routes/feed.tsx`)
2929+3030+- Cards now use `.island-shell` (frosted glass style) instead of bare `border rounded-lg`
3131+- Outer wrapper uses `bg-[--bg-base]` for the page background color token
3232+2833## Current state of key files
29343035```
3136src/lib/db.ts ← SQLite setup, creates oauth_state + oauth_session tables
3237src/lib/oauth-client.ts ← NodeOAuthClient with DB-backed stateStore + sessionStore
3338src/routes/callback.tsx ← sets DID cookie, redirects to /feed
3434-src/routes/feed.tsx ← createServerFn fetches timeline; basic card UI (needs polish)
3939+src/routes/feed.tsx ← createServerFn fetches timeline; island-shell card UI
3540src/routes/login.tsx ← plain unstyled form; working, needs polish
3641src/routes/__root.tsx ← shell with Header + Footer components, devtools, theme init script
3742src/components/Header.tsx ← sticky nav with "Feed" link chip; uses design tokens
···3944src/styles.css ← Tailwind v4 + custom design system (see below)
4045```
41464242-## What needs to be done: UI cleanup
4343-4444-The auth flow and data loading all work end-to-end. The feed renders but looks rough. The next session is purely visual polish — no logic changes needed.
4545-4646-### Design system already in place
4747+## Design system
47484848-`src/styles.css` has a full set of CSS custom properties and utility classes to use:
4949+`src/styles.css` has a full set of CSS custom properties and utility classes:
49505051**Color tokens** (light + dark + `prefers-color-scheme` variants):
5152- `--sea-ink` / `--sea-ink-soft` — primary text colors
···6667- `.rise-in` — entrance animation (opacity + translateY, 700ms)
6768- `.feature-card` — card with hover lift
68697070+To use design tokens as Tailwind values: `text-[--sea-ink-soft]`, `bg-[--surface]`, `border-[--line]` etc. Tailwind v4 wraps `--` prefixed values in `var()` automatically.
7171+6972**Fonts**: Manrope (sans body), Fraunces (serif display)
70737171-### Specific things to polish
7474+## Remaining UI polish (carry-over)
7575+7676+These were not finished last session:
7777+7878+**`src/routes/feed.tsx`:**
7979+- `text-gray-500` on the handle should be `text-[--sea-ink-soft]` (hardcoded color, won't respect dark mode)
8080+- No timestamp — add `createdAt: (item.post.record as { createdAt?: string }).createdAt ?? null` to the mapped return in `getTimeline`, then render it
8181+- No avatar fallback for posts where `author.avatar` is null
8282+- No `pendingComponent` or `errorComponent` on the route
8383+8484+**`src/routes/login.tsx`:**
8585+- Form has zero styling — needs centered card layout, `.island-shell`, lagoon accent on the button
8686+8787+**`src/routes/__root.tsx`:**
8888+- Page title is still "TanStack Start Starter" — should be "Dudesky"
8989+9090+## Next: threaded reply views
9191+9292+Goal: clicking a post opens a thread view showing the post, its ancestors (parent chain), and its replies.
72937373-**`src/routes/feed.tsx` — the main job:**
7474-- Post cards are bare `border rounded-lg` — should use `.island-shell` and the design token colors
7575-- Avatar images have no fallback for missing avatars
7676-- No timestamp on posts (it's available: `(item.post.record as { createdAt?: string }).createdAt`)
7777-- The feed data shape currently mapped in `getTimeline` only extracts `uri`, `text`, `author.{handle,displayName,avatar}` — if you need `createdAt` or other fields, add them to the mapped return object in the server function
7878-- No loading/pending state (`pendingComponent` on the route)
7979-- No error state (`errorComponent` on the route)
9494+### ATProto API
9595+9696+`agent.getPostThread({ uri })` returns a `ThreadViewPost` which has:
9797+- `post` — the post itself
9898+- `parent` — `ThreadViewPost | NotFoundPost | BlockedPost | undefined` (walk up for ancestors)
9999+- `replies` — `ThreadViewPost[] | ...` (direct replies)
100100+101101+The `uri` for each post is already in the mapped feed data as `item.uri`.
102102+103103+### Suggested route
104104+105105+`src/routes/post.$uri.tsx` — file-based dynamic route. The `uri` param will be the AT URI, but since it contains `/` characters (`at://did:plc:.../app.bsky.feed.post/...`) it needs to be encoded when used as a URL segment (use `encodeURIComponent` / `decodeURIComponent`).
106106+107107+### Data shape to map in the server fn
108108+109109+Same `createServerFn` pattern as `feed.tsx`. Map to a plain serializable object — don't pass raw ATProto types through. Suggested shape:
801108181-**`src/routes/login.tsx` — unstyled:**
8282-- The form (`<input>`, `<button>`) has zero styling
8383-- Should match the app's aesthetic — centered card layout, `.island-shell`, lagoon accent on the button
111111+```ts
112112+{
113113+ post: { uri, text, author, createdAt },
114114+ ancestors: Array<{ uri, text, author, createdAt }>, // ordered root-first
115115+ replies: Array<{ uri, text, author, createdAt }>,
116116+}
117117+```
841188585-**`src/routes/__root.tsx` — minor:**
8686-- Page title is still "TanStack Start Starter" — should be "Dudesky" or similar
119119+Walk `parent` recursively to build `ancestors`, collect `replies` array directly.
8712088121## Conventions
89122···93126- `createServerFn` for any server-only code called from loaders (DB, cookies, secrets)
94127- Env vars: `VITE_APP_URL`, `PRIVATE_KEY_0/1/2`, optionally `DB_PATH`
95128- Read AGENTS.md and load the relevant SKILL.md files before working on TanStack-related tasks
129129+- Playwright is configured for WSL2 with `--no-sandbox` in `~/.claude/plugins/cache/claude-plugins-official/playwright/unknown/.mcp.json`
130130+- To inject auth into the headless browser: set cookie `did=<url-encoded-did>` on `localhost:3001` via `browser_evaluate`, then navigate to `/feed`