docs(webui): implementation plan for UI polish

This commit is contained in:
m4
2026-08-10 14:06:13 +08:00
parent a9ee44e7dd
commit d0ab5b6e7f
@@ -0,0 +1,627 @@
# UI Polish — Bright/Premium Theme Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Restyle the WebUI to a bright, premium (Stripe-like) theme: zinc-neutral surfaces, vivid cyan accents, Inter font, layered shadows — across the main UI, all dialogs, and the login page, in both light and dark mode.
**Architecture:** Token-first, three independently shippable layers: (1) rewrite CSS design tokens in `globals.css` + wire Inter via `next/font/local`; (2) restyle the `src/components/ui/` primitives so every consumer inherits; (3) targeted screen-level class edits (chat area, thread list, login). Purely visual — no logic changes.
**Tech Stack:** Tailwind CSS 3 (config `tailwind.config.mjs`, `darkMode: "class"`), shadcn-style Radix primitives, `next/font/local`, Vitest.
**Spec:** `docs/superpowers/specs/2026-08-10-ui-polish-bright-design.md`
## Global Constraints
- This is a purely visual change: `npx tsc --noEmit`, `npx eslint .`, and `npm test` must stay green after every task. Any test breakage means logic was touched by accident — revert it.
- Commit style: `feat(webui): …` / `style(webui): …`.
- The working tree contains **unrelated uncommitted user changes**, including in `src/app/components/ChatInterface.tsx` and `src/app/components/ChatMessage.tsx`. Tasks that edit those files must NOT `git add` them wholesale — the controller stages only the task's hunks. Never use `git add -A` / `git add .` anywhere.
- `src/app/components/ContextUsageIndicator.tsx` is untracked user work-in-progress — do not touch it. (Spec's `tnum` application is descoped to shipping the `.tnum` utility only; see Self-Review Notes.)
- Dark mode is driven by the `.dark` class on `<html>`; both `:root` and `.dark` token sets are updated in the same task.
- WCAG AA for text: cyan-600 `#0891b2` on white; cyan-700 `#0e7490` on cyan-50 `#ecfeff`; dark-mode equivalents.
---
### Task 1: Design tokens + Inter font
**Files:**
- Create: `src/app/fonts/InterVariable.woff2` (downloaded font file)
- Modify: `src/app/globals.css` (token blocks, body font, heading tracking, `.tnum` utility)
- Modify: `src/app/layout.tsx` (font wiring, viewport themeColor)
**Interfaces:**
- Produces (consumed by Tasks 2–4):
- Color tokens: `--brand` `#0891b2` (light) / `#22d3ee` (dark); `--brand-solid`; `--color-active-bg` `#ecfeff` / `#083344`; `--color-active-text` `#0e7490` / `#67e8f9`; remapped shadcn HSL tokens (`--background`, `--muted`, `--accent`, `--ring`, …)
- Shadow tokens: `--shadow-sm` / `--shadow-lg` (popover) / `--shadow-xl` (dialog), overridden in `.dark`
- `--radius: 0.625rem` (so Tailwind `rounded-md` = 8px, `rounded-lg` = 10px)
- Font: `--font-inter` CSS variable on `<body>`; `.tnum` utility class
- [ ] **Step 1: Download the Inter variable font**
Run:
```bash
mkdir -p src/app/fonts
curl -fL -o src/app/fonts/InterVariable.woff2 "https://rsms.me/inter/font-files/InterVariable.woff2?v=4.1"
```
Verify: `ls -la src/app/fonts/InterVariable.woff2` shows a file of roughly 800 KB – 1.5 MB, and `file src/app/fonts/InterVariable.woff2` reports `Web/OpenType font` (or `data` — woff2 has no magic string in some `file` builds; size is the real check).
Fallback if rsms.me is unreachable:
```bash
curl -fL -o /tmp/inter.zip https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip
unzip -l /tmp/inter.zip | grep -i "InterVariable.woff2" # locate the exact path inside
unzip -j -o /tmp/inter.zip "<path-from-listing>" -d src/app/fonts/
```
If both fail (offline environment), STOP and report BLOCKED — do not substitute a different font or a CDN link.
- [ ] **Step 2: Wire the font in `src/app/layout.tsx`**
Add the import after the existing imports (line 8 area):
```ts
import localFont from "next/font/local";
```
Add the font definition after the `viewport` export (after line 22):
```ts
// Self-hosted Inter variable font; exposes --font-inter, consumed by globals.css.
// No CJK glyphs — Chinese text falls through to PingFang/YaHei in the stack.
const inter = localFont({
src: "./fonts/InterVariable.woff2",
variable: "--font-inter",
display: "swap",
});
```
Change the `<body>` tag (line 41) from:
```tsx
<body suppressHydrationWarning>
```
to:
```tsx
<body
suppressHydrationWarning
className={inter.variable}
>
```
Update the stale viewport theme colors (lines 17-20) from:
```ts
{ media: "(prefers-color-scheme: light)", color: "#f9f9f9" },
{ media: "(prefers-color-scheme: dark)", color: "#212121" },
```
to:
```ts
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
{ media: "(prefers-color-scheme: dark)", color: "#09090b" },
```
- [ ] **Step 3: Replace the light `:root` token block in `src/app/globals.css`**
In `src/app/globals.css`, inside `@layer base { :root { … } }`, replace everything from the comment `/* App-specific color variables */` (line 13) through `--chart-5: 330 26% 48%;` (line 144) with the block below. Keep: the alias block (`--text-primary: var(--color-text-primary);` … `--scrollbar-thumb-hover`) and the spacing/font-size/line-height/weight/radius-sm..full/layout variable sections exactly as they are — the replacement block below re-includes only what it replaces; anything not shown here stays untouched. (The old `--radius-sm/md/lg/full` and `--sidebar-width` etc. sections sit between the color section and the Tailwind variables section — leave them in place.)
```css
/* App-specific colors — bright neutral palette. Light mode: zinc-50 app
background, white surfaces, near-black zinc text, vivid cyan accent. */
--color-primary: #18181b;
--color-user-message: #0e7490;
--color-user-message-bg: #ecfeff;
--color-avatar-bg: #cffafe;
--color-secondary: #52525b;
--color-success: #16a34a;
--color-warning: #d97706;
--color-error: #dc2626;
--color-background: #fafafa;
--color-subagent-hover: #f4f4f5;
--color-surface: #ffffff;
--color-border: #e4e4e7;
--color-border-light: #f4f4f5;
--color-text-primary: #18181b;
--color-text-secondary: #52525b;
--color-text-tertiary: #a1a1aa;
--color-file-button: #ffffff;
--color-file-button-hover: #f4f4f5;
/* Active/selected surface (sidebar items, tabs, selections). */
--color-active-bg: #ecfeff;
--color-active-text: #0e7490;
/* Brand accent — EvoScientist cyan. cyan-600 passes WCAG AA on white. */
--brand: #0891b2;
--brand-hover: #0e7490;
/* Flat fill for badges/marks; the primary Button adds its own gradient. */
--brand-solid: #0891b2;
--brand-foreground: #ffffff;
```
Then, still inside `:root`, replace the shadow variables block (lines 102-106, `/* Shadow variables */` through the `--shadow-xl` line) with:
```css
/* Shadow variables — lighter, layered. lg = popover/dropdown, xl = dialog. */
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
--shadow-lg: 0 4px 12px -2px rgb(0 0 0 / 0.08),
0 2px 4px -2px rgb(0 0 0 / 0.04);
--shadow-xl: 0 20px 40px -12px rgb(0 0 0 / 0.18),
0 4px 12px -4px rgb(0 0 0 / 0.06);
```
And replace the Tailwind/Radix HSL variables (lines 118-144, from `--radius: 0.5rem;` through `--chart-5`) with:
```css
/* Tailwind/Radix UI component variables */
--radius: 0.625rem;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 189 90% 36%;
--primary-foreground: 0 0% 100%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 5% 34%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 10% 3.9%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 189 90% 36%;
--sidebar: 0 0% 98%;
--chart-1: 189 90% 36%;
--chart-2: 173 80% 40%;
--chart-3: 38 92% 50%;
--chart-4: 199 89% 48%;
--chart-5: 350 89% 60%;
```
- [ ] **Step 4: Replace the `.dark` token block**
Inside the same file's `.dark { … }` (lines 151-206), replace everything from `/* App-specific colors — warm charcoal dark mode …` through `--chart-5: 332 39% 64%;` with:
```css
/* App-specific colors — cool neutral dark (zinc), cyan accents. */
--color-primary: #fafafa;
--color-user-message: #67e8f9;
--color-user-message-bg: #083344;
--color-avatar-bg: #083344;
--color-secondary: #a1a1aa;
--color-success: #4ade80;
--color-warning: #fbbf24;
--color-error: #f87171;
--color-background: #09090b;
--color-subagent-hover: #27272a;
--color-surface: #18181b;
--color-border: #27272a;
--color-border-light: #1f1f22;
--color-text-primary: #fafafa;
--color-text-secondary: #a1a1aa;
--color-text-tertiary: #71717a;
--color-file-button: #18181b;
--color-file-button-hover: #27272a;
--color-active-bg: #083344;
--color-active-text: #67e8f9;
--brand: #22d3ee;
--brand-hover: #67e8f9;
/* Deep, saturated cyan fill: colorful but low-lightness so buttons don't
glow on the dark surface; white text passes AA. */
--brand-solid: #0e7490;
--brand-foreground: #ffffff;
/* Shadows need more contrast on dark surfaces. */
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
--shadow-lg: 0 4px 12px -2px rgb(0 0 0 / 0.5),
0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-xl: 0 20px 40px -12px rgb(0 0 0 / 0.6),
0 4px 12px -4px rgb(0 0 0 / 0.4);
/* Tailwind/Radix UI component variables for dark mode */
--radius: 0.625rem;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 192 86% 29%;
--primary-foreground: 0 0% 100%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 187 85% 53%;
--sidebar: 240 6% 8%;
--chart-1: 187 85% 53%;
--chart-2: 172 66% 50%;
--chart-3: 43 96% 56%;
--chart-4: 198 93% 60%;
--chart-5: 351 95% 71%;
```
- [ ] **Step 5: Body font stack, heading tracking, `.tnum` utility**
In the same file, change the `body` rule's `font-family` (lines 234-235) from:
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
```
to:
```css
font-family: var(--font-inter), -apple-system, BlinkMacSystemFont,
"Segoe UI", "PingFang SC", "Microsoft YaHei", Roboto, "Helvetica Neue",
Arial, sans-serif;
```
Change the `h1, h2, h3, h4, h5, h6` rule (lines 244-252) from:
```css
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.25;
color: var(--color-text-primary);
}
```
to:
```css
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.25;
letter-spacing: -0.02em;
color: var(--color-text-primary);
}
```
Append at the end of the file:
```css
/* Tabular numerals for token/usage readouts. */
.tnum {
font-feature-settings: "tnum";
}
```
- [ ] **Step 6: Verify**
Run: `npx tsc --noEmit && npx eslint src/app/layout.tsx && npm test`
Expected: no type errors, no lint errors, all tests pass (310 passed / 1 skipped at plan time).
- [ ] **Step 7: Commit**
These files are NOT in the user's uncommitted set — stage them directly:
```bash
git add src/app/globals.css src/app/layout.tsx src/app/fonts/InterVariable.woff2
git commit -m "style(webui): bright zinc+cyan design tokens and Inter font"
```
- [ ] **Step 8 (controller, not implementer):** Hand to the user for the Layer-1 visual gate (light + dark overall palette/feel) per the spec.
---
### Task 2: `ui/` primitives restyle
**Files:**
- Modify: `src/components/ui/button.tsx`
- Modify: `src/components/ui/input.tsx`
- Modify: `src/components/ui/textarea.tsx`
- Modify: `src/components/ui/select.tsx`
- Modify: `src/components/ui/dialog.tsx`
- Modify: `src/components/ui/dropdown-menu.tsx`
**Interfaces:**
- Consumes (from Task 1): `--brand`, `--brand-solid`, `--brand-hover`, `--brand-foreground`, `--shadow-lg`, `--shadow-xl`.
- Produces: every existing consumer of Button/Input/Select/Dialog/DropdownMenu inherits the new look without edits (Task 3–4 rely on this).
All edits are exact string replacements inside `cn(...)`/cva class strings. No logic changes.
- [ ] **Step 1: Button — gradient primary variant**
In `src/components/ui/button.tsx`, replace the `default` variant string (line 13):
```ts
default:
"bg-[var(--brand-solid)] text-[var(--brand-foreground)] shadow-xs hover:opacity-90",
```
with:
```ts
default:
"bg-gradient-to-b from-[#06b6d4] to-[var(--brand-solid)] text-[var(--brand-foreground)] shadow-sm hover:from-[#0891b2] hover:to-[var(--brand-hover)] dark:bg-none dark:bg-[var(--brand-solid)] dark:hover:bg-[var(--brand-hover)]",
```
(Light: subtle vertical cyan-500→cyan-600 gradient deepening on hover. Dark: flat `--brand-solid` — a gradient would glow.)
- [ ] **Step 2: Input — cyan focus treatment**
In `src/components/ui/input.tsx`, replace (line 12):
```ts
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
```
with:
```ts
"focus-visible:border-[var(--brand)] focus-visible:ring-[3px] focus-visible:ring-[var(--brand)]/20",
```
- [ ] **Step 3: Textarea — same focus treatment**
In `src/components/ui/textarea.tsx`, replace (line 12):
```ts
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
```
with:
```ts
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:border-[var(--brand)] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--brand)]/20 disabled:cursor-not-allowed disabled:opacity-50",
```
- [ ] **Step 4: Select — trigger focus + content/item polish**
In `src/components/ui/select.tsx`:
a) Trigger (line 22) — replace:
```ts
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1",
```
with:
```ts
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm focus:border-[var(--brand)] focus:outline-none focus:ring-[3px] focus:ring-[var(--brand)]/20 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1",
```
b) Content (line 78) — replace `rounded-md border bg-popover text-popover-foreground shadow-md` with `rounded-xl border bg-popover text-popover-foreground shadow-[var(--shadow-lg)]` (keep every other class in that string untouched).
c) Item (line 121) — replace `rounded-sm` with `rounded-md` in the SelectItem class string.
- [ ] **Step 5: Dialog — blur overlay, 16px radius, layered shadow**
In `src/components/ui/dialog.tsx`:
a) Overlay (line 61) — replace:
```ts
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
```
with:
```ts
"fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
```
b) Content (line 83) — replace `rounded-lg border bg-background p-6 shadow-lg` with `rounded-2xl border bg-background p-6 shadow-[var(--shadow-xl)]` (keep every other class untouched, including the max-height/translate/animation classes).
- [ ] **Step 6: Dropdown — rounded content + items, layered shadow**
In `src/components/ui/dropdown-menu.tsx`:
a) Content (line 63) — replace `rounded-md border bg-popover p-1 text-popover-foreground shadow-md` with `rounded-xl border bg-popover p-1 text-popover-foreground shadow-[var(--shadow-lg)]`.
b) Item (line 96) — replace `rounded-sm` with `rounded-md` in the DropdownMenuItem class string.
- [ ] **Step 7: Verify**
Run: `npx tsc --noEmit && npx eslint src/components/ui/ && npm test`
Expected: clean, all tests pass.
- [ ] **Step 8: Commit**
```bash
git add src/components/ui/button.tsx src/components/ui/input.tsx src/components/ui/textarea.tsx src/components/ui/select.tsx src/components/ui/dialog.tsx src/components/ui/dropdown-menu.tsx
git commit -m "style(webui): restyle ui primitives (gradient primary, cyan focus, layered shadows)"
```
- [ ] **Step 9 (controller, not implementer):** Hand to the user for the Layer-3 visual gate (dialogs, buttons, inputs, dropdowns — hover/focus/disabled, both modes).
---
### Task 3: Chat area polish
**Files:**
- Modify: `src/app/components/ChatInterface.tsx:2048` (composer container)
- Modify: `src/app/components/ChatMessage.tsx:274` (user bubble)
- Modify: `src/app/components/ToolCallBox.tsx:305` (container radius)
**Interfaces:**
- Consumes (from Task 1): `--brand`, `--shadow-sm`, `--color-user-message-bg` (now `#ecfeff` light / `#083344` dark — applied via the existing inline style, no code change needed).
- Consumes (from Task 2): the composer Send/Stop `Button` already renders the gradient primary variant — no edit.
**Commit discipline (unusual — read first):** `ChatInterface.tsx` and `ChatMessage.tsx` contain unrelated uncommitted user changes. The implementer makes the edits and runs verification but does NOT commit; the controller stages only the task's hunks (`git apply --cached` with a filtered diff) and commits. `ToolCallBox.tsx` is clean and committed normally by the controller too.
- [ ] **Step 1: Composer container — 16px radius, subtle shadow, cyan focus-within ring**
In `src/app/components/ChatInterface.tsx`, find (line 2048, first element of a `cn(` call):
```ts
"relative z-10 mx-auto mb-2 flex w-[calc(100%-16px)] max-w-[960px] flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-background transition-colors duration-200 ease-in-out sm:mb-4 sm:w-[calc(100%-24px)]",
```
Replace with:
```ts
"relative z-10 mx-auto mb-2 flex w-[calc(100%-16px)] max-w-[960px] flex-shrink-0 flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-[var(--shadow-sm)] transition-colors duration-200 ease-in-out focus-within:border-[var(--brand)] focus-within:ring-2 focus-within:ring-[var(--brand)]/20 sm:mb-4 sm:w-[calc(100%-24px)]",
```
- [ ] **Step 2: User message bubble — 16px radius, borderless**
In `src/app/components/ChatMessage.tsx`, find (line 274, inside the `isUser` ternary):
```ts
? "rounded-xl rounded-br-none border border-border px-3 py-2 text-foreground"
```
Replace with:
```ts
? "rounded-2xl rounded-br-md px-3.5 py-2 text-foreground"
```
(The bubble's background keeps coming from the inline `backgroundColor: "var(--color-user-message-bg)"` a few lines below — Task 1 changed that token to cyan-50/dark-cyan, so the bubble restyles with no further edit.)
- [ ] **Step 3: ToolCallBox container radius**
In `src/app/components/ToolCallBox.tsx`, find (line 305):
```ts
className="rounded-sm border border-border"
```
Replace with:
```ts
className="rounded-lg border border-border"
```
- [ ] **Step 4: Verify (implementer)**
Run: `npx tsc --noEmit && npx eslint src/app/components/ChatInterface.tsx src/app/components/ChatMessage.tsx src/app/components/ToolCallBox.tsx && npm test`
Expected: clean, all tests pass.
- [ ] **Step 5: Selective commit (controller, not implementer)**
The controller extracts only the hunks containing the three replacement strings above from the working-tree diff, stages them with `git apply --cached`, and commits:
```bash
git commit -m "style(webui): polish composer, user bubble, and tool-call box"
```
- [ ] **Step 6 (controller):** Hand to the user for the chat-area visual gate (both modes).
---
### Task 4: Thread list + login page polish
**Files:**
- Modify: `src/app/components/ThreadList.tsx` (active states ×4)
- Modify: `src/app/login/page.tsx` (login card)
**Interfaces:**
- Consumes (from Task 1): `--color-active-bg`, `--color-active-text`, `--shadow-xl`.
Both files are clean of user changes — normal staging.
- [ ] **Step 1: Thread list — active conversation uses the cyan active surface**
In `src/app/components/ThreadList.tsx`, find (line 462, the active branch of a ternary):
```ts
? "border border-primary bg-accent hover:bg-accent"
```
Replace with:
```ts
? "border border-transparent bg-[var(--color-active-bg)] text-[var(--color-active-text)] hover:bg-[var(--color-active-bg)]"
```
- [ ] **Step 2: Sidebar view tabs — active state uses the cyan active surface**
In the same file, replace each of these three strings:
```ts
view === "skills" && "bg-accent"
```
```ts
view === "memory" && "bg-accent"
```
```ts
view === "schedule" && "bg-accent"
```
with, respectively:
```ts
view === "skills" &&
"bg-[var(--color-active-bg)] text-[var(--color-active-text)]"
```
```ts
view === "memory" &&
"bg-[var(--color-active-bg)] text-[var(--color-active-text)]"
```
```ts
view === "schedule" &&
"bg-[var(--color-active-bg)] text-[var(--color-active-text)]"
```
- [ ] **Step 3: Login card — 16px radius, dialog shadow, more padding**
In `src/app/login/page.tsx`, find (line 79):
```ts
className="w-full max-w-sm rounded-lg border border-border bg-[var(--color-surface)] p-6 shadow-sm"
```
Replace with:
```ts
className="w-full max-w-sm rounded-2xl border border-border bg-[var(--color-surface)] p-8 shadow-[var(--shadow-xl)]"
```
(The sign-in button already uses the shared `Button` default variant → gradient from Task 2; inputs inherit Task 2's focus treatment.)
- [ ] **Step 4: Verify**
Run: `npx tsc --noEmit && npx eslint src/app/components/ThreadList.tsx src/app/login/page.tsx && npm test`
Expected: clean, all tests pass.
- [ ] **Step 5: Commit**
```bash
git add src/app/components/ThreadList.tsx src/app/login/page.tsx
git commit -m "style(webui): cyan active states in sidebar, premium login card"
```
- [ ] **Step 6 (controller):** Hand to the user for the final visual gate (thread list active states, login page, both modes).
---
## Self-Review Notes (already applied)
- **Spec coverage:** Layer 1 → Task 1 (tokens, font, typography, shadows, radii, both modes). Layer 2 → Task 1 Step 2/5 (font wiring, tracking) + Task 1 Step 5 (`.tnum` utility). Layer 3 → Task 2 (all six primitives). Layer 4 → Tasks 3–4 (chat, thread list, login). Workspace panel needs **no** edits: its checkboxes use `accent-[var(--brand)]` (= cyan-600 after Task 1) and its category labels already match the 11px uppercase micro style — token remap carries it, verified by reading `WorkspacePanel.tsx`.
- **Descoped from spec, deliberately:** (a) blanket "more breathing room between messages" — no concrete receiver could be pinned without redesigning message layout; the lighter tokens already open the UI up, and the user's per-layer visual gates are the right place to request specific spacing follow-ups. (b) Applying `.tnum` to specific numeric readouts — the natural target (`ContextUsageIndicator.tsx`) is untracked user WIP and must not be swept into a commit; the utility ships ready to use.
- **Placeholder scan:** every edit step contains exact old/new code strings with line anchors.
- **Type/consistency:** token names referenced in Tasks 2–4 (`--brand`, `--brand-solid`, `--brand-hover`, `--color-active-bg`, `--color-active-text`, `--shadow-sm/lg/xl`) are all produced by Task 1.
- **Risk:** Task 1 Step 3 instructs replacing a large token block while *keeping* the alias/spacing/layout sections that interleave it — the implementer must read the current file and preserve everything not explicitly replaced. The brief tells them exactly which line ranges are replaced.