docs: boocode-lift-analysis, openspec change docs, codesight cache, deps
- Add boocode-lift-analysis.md: comprehensive 30-repo lift matrix across 25 domains - Add openspec/ change docs: domain2-code-intelligence, domain3-multi-agent, impeccable-wave, streaming-codeblocks - Update .gitignore: .impeccable/, .omo/, bun.lock, DESIGN.md, PRODUCT.md - Update dependencies in package.json + pnpm-lock.yaml - Update .codesight/ analysis cache
This commit is contained in:
300
openspec/changes/impeccable-wave/design.md
Normal file
300
openspec/changes/impeccable-wave/design.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Design Decisions — Impeccable Wave
|
||||
|
||||
## 1. Motion Token System
|
||||
|
||||
### Approach
|
||||
|
||||
Define CSS custom properties for motion in `apps/web/src/styles/globals.css`, then reference from components. This is the standard pattern (CSS custom properties) that integrates natively with Tailwind v4's `@theme` directive.
|
||||
|
||||
### Token Schema
|
||||
|
||||
```css
|
||||
@theme {
|
||||
/* Durations */
|
||||
--duration-instant: 0ms;
|
||||
--duration-fast: 100ms;
|
||||
--duration-normal: 150ms;
|
||||
--duration-slow: 200ms;
|
||||
--duration-deliberate: 300ms;
|
||||
|
||||
/* Easing curves */
|
||||
--ease-enter: cubic-bezier(0.22, 1, 0.36, 1); /* entrances, button press */
|
||||
--ease-move: cubic-bezier(0.25, 1, 0.5, 1); /* slides, drawers, panels */
|
||||
--ease-hover: ease; /* hover states, color transitions */
|
||||
--ease-emerge: cubic-bezier(0.165, 0.84, 0.44, 1); /* modals, popovers scaling in */
|
||||
--ease-exit: cubic-bezier(0.4, 0, 0.2, 1); /* quick dismissals */
|
||||
|
||||
/* Reduced motion override */
|
||||
--motion-reduce-transform: none;
|
||||
--motion-reduce-opacity: none;
|
||||
}
|
||||
```
|
||||
|
||||
The `.dark` block or `@media (prefers-reduced-motion: reduce)` overrides `--motion-reduce-*` to `none` and sets duration overrides to `0ms`.
|
||||
|
||||
### Usage pattern
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
className="transition-colors duration-150"
|
||||
|
||||
// After
|
||||
className="transition-colors duration-[var(--duration-fast)] ease-[var(--ease-hover)]"
|
||||
```
|
||||
|
||||
For framer-motion, reference same tokens via JS constants in `lib/motion.ts`.
|
||||
|
||||
## 2. Animation Library Choice: framer-motion
|
||||
|
||||
### Why framer-motion over alternatives
|
||||
|
||||
| Feature | framer-motion | motion (standalone) | CSS-only |
|
||||
|---------|--------------|---------------------|----------|
|
||||
| Layout animations | `layout` prop | `layout` prop | ❌ Requires JS |
|
||||
| AnimatePresence | ✅ | ✅ | ❌ |
|
||||
| Spring physics | ✅ | ✅ | ❌ (CSS steps approximation) |
|
||||
| Gesture handling | `drag`, `whileHover`, `whileTap` | Same API | Partial |
|
||||
| Stagger children | `staggerChildren` variant | Same | ❌ |
|
||||
| Bundle size | ~30KB gzipped | ~10KB gzipped | 0KB |
|
||||
| React 18 compat | ✅ 12.x | ✅ 12.x | N/A |
|
||||
|
||||
**Decision:** Use `framer-motion` (full-featured). The `motion` standalone package (same API, smaller bundle) is a future migration if bundle size becomes a concern. For this wave, framer-motion's broader ecosystem and established React 18 support are the safe choice.
|
||||
|
||||
### Bundle impact
|
||||
|
||||
`framer-motion` v12 adds ~30KB gzipped. Route-level code splitting in Phase 5 will offset this by reducing the initial bundle by an estimated 100-200KB (the current bundle includes all 6 page components). Net bundle impact: **~70KB reduction** after code splitting.
|
||||
|
||||
## 3. Workspace Pane Animation Strategy
|
||||
|
||||
### Current architecture
|
||||
|
||||
The workspace grid (`Workspace.tsx`) renders panes from `panes[]` array in a CSS grid. Pane open/close is a state mutation — panes appear/disappear instantly. Reorder is handled by drag (HTML5 native).
|
||||
|
||||
### Animated approach
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Workspace Grid │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Chat │ │ Terminal │ │
|
||||
│ │ (enter: │ │ (enter: │ │
|
||||
│ │ slideL) │ │ slideR) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ Coder (enter: slideUp) │ │
|
||||
│ └──────────────────────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
1. **Pane open**: Slide in from the nearest edge (left-pane opens from left, right-pane from right, bottom-pane from bottom) with fade. 200ms `ease-out`.
|
||||
2. **Pane close**: Quick fade (100ms) — exit should be faster than enter per animation best practices.
|
||||
3. **Pane reorder**: `layout` prop on each pane wrapper. framer-motion handles the FLIP calculation automatically.
|
||||
4. **Tab switch**: Content crossfade (150ms). Not a slide — tabs are content replacement, not navigation.
|
||||
5. **Split pane**: New pane emerges from the split point (scale + fade, `transform-origin` at the split edge).
|
||||
|
||||
### Implementation
|
||||
|
||||
```tsx
|
||||
// Workspace.tsx — pane wrapper
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.1 } }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
>
|
||||
<PaneComponent />
|
||||
</motion.div>
|
||||
```
|
||||
|
||||
`AnimatePresence` wraps the pane grid so exiting panes animate out before DOM removal.
|
||||
|
||||
## 4. Message Stagger Entrance
|
||||
|
||||
### Approach
|
||||
|
||||
Use framer-motion's `staggerChildren` variant on the message list container, with individual item variants for the entrance.
|
||||
|
||||
```tsx
|
||||
const containerVariants = {
|
||||
initial: {},
|
||||
animate: {
|
||||
transition: {
|
||||
staggerChildren: 0.03, // 30ms between messages
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const messageVariants = {
|
||||
initial: { opacity: 0, y: 8 },
|
||||
animate: { opacity: 1, y: 0, transition: { duration: 0.2, ease: 'easeOut' } },
|
||||
};
|
||||
```
|
||||
|
||||
**Edge cases:**
|
||||
- **New messages during streaming**: Only animate the first appearance of a message. Once visible, subsequent content updates (streaming deltas) should not re-animate. Track via `data-animated` attribute.
|
||||
- **Scroll-anchored messages**: When the user is scrolled to bottom and a new message arrives, animate it in. When the user has scrolled up, suppress animation to avoid disorientation.
|
||||
- **Initial load**: All existing (already-loaded) messages appear without animation. Only NEW messages animate in.
|
||||
- **Reasoning block**: The collapsible reasoning block uses a spring-based height transition instead of the grid-rows CSS trick.
|
||||
|
||||
## 5. Keyboard Shortcuts Dialog
|
||||
|
||||
### Invocation
|
||||
|
||||
`Cmd+/` or `?` in the workspace. A modal overlay triggered via a new keyboard handler in `Session.tsx`.
|
||||
|
||||
### Content
|
||||
|
||||
Grouped into sections:
|
||||
|
||||
```
|
||||
Navigation │ Cmd+` Terminal │ Cmd+T New Terminal
|
||||
│ Cmd+C New Chat │ Cmd+W Close Pane
|
||||
│ Tab / Shift+Tab │ Cycle panes
|
||||
|
||||
Chat │ Enter Send │ Shift+Enter Newline
|
||||
│ Cmd+[ 1-9 ] │ Jump to tab
|
||||
│ @ │ Mention file
|
||||
│ / │ Slash commands
|
||||
|
||||
Terminal │ Cmd+Shift+C │ Copy
|
||||
│ Cmd+F │ Search
|
||||
│ Esc │ Close search
|
||||
|
||||
General │ Cmd+/ │ This panel
|
||||
│ ? │ This panel
|
||||
```
|
||||
|
||||
### Data source
|
||||
|
||||
Derived from existing keyboard handlers in `Session.tsx` and component files. Centralized into a single `KEYBOARD_SHORTCUTS` constant in `lib/keyboard-shortcuts.ts`.
|
||||
|
||||
## 6. Undo Toast System
|
||||
|
||||
### Approach
|
||||
|
||||
Extend the existing Sonner toast system. Destructive actions call `toast.success(message, { action: { label: 'Undo', onClick: rollback } })`. Each action needs a rollback function.
|
||||
|
||||
### Implementation pattern
|
||||
|
||||
```typescript
|
||||
// In component with destructive action
|
||||
async function deleteChat(chatId: string) {
|
||||
const previousMessages = await api.chats.messages.list(chatId); // snapshot
|
||||
await api.chats.remove(chatId); // optimistic delete
|
||||
|
||||
toast.success('Chat deleted', {
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: async () => {
|
||||
await api.chats.restore(chatId, previousMessages); // rollback
|
||||
},
|
||||
},
|
||||
duration: 5000, // 5s to undo
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
Start with 3 high-impact undo actions:
|
||||
1. **Delete chat** — snapshot messages, restore on undo
|
||||
2. **Archive project** — toggle unarchive
|
||||
3. **Close pane** — reopen via `closedPaneStack` (already exists)
|
||||
|
||||
## 7. Font Pairing Strategy
|
||||
|
||||
### Goals
|
||||
- Distinctive but still technical/developer-appropriate
|
||||
- Must pair well with JetBrains Mono (code font stays)
|
||||
- Must support variable weights for the Tailwind v4 font-weight system
|
||||
- Must be available on Google Fonts (self-hosted via `@fontsource`)
|
||||
|
||||
### Candidate pairs
|
||||
|
||||
| Pair | Vibe | Google Fonts | Variable |
|
||||
|------|------|-------------|----------|
|
||||
| **Instrument Sans** + JetBrains Mono | Modern, geometric, technical | ✅ | ✅ |
|
||||
| **Public Sans** + JetBrains Mono | Clean, neutral, authoritative | ✅ | ✅ |
|
||||
| **Satoshi** + JetBrains Mono | Bold, editorial, confident | ❌ (paid) | ✅ |
|
||||
| **Onest** + JetBrains Mono | Rounded, approachable but tech | ✅ | ✅ |
|
||||
| **Plus Jakarta Sans** + JetBrains Mono | Refined, slightly warmer | ✅ | ✅ |
|
||||
|
||||
**Decision:** Defer final choice to implementation. Replace Inter with chosen font via `@fontsource` import in `main.tsx` + update the `--font-sans` token in `globals.css`.
|
||||
|
||||
## 8. Code Splitting Architecture
|
||||
|
||||
### Current
|
||||
```tsx
|
||||
// App.tsx — static imports, all loaded upfront
|
||||
import Home from '@/pages/Home'
|
||||
import Project from '@/pages/Project'
|
||||
import Session from '@/pages/Session'
|
||||
import Settings from '@/pages/Settings'
|
||||
import Analytics from '@/pages/Analytics'
|
||||
import Results from '@/pages/Results'
|
||||
```
|
||||
|
||||
### Target
|
||||
```tsx
|
||||
// App.tsx — dynamic imports with named exports
|
||||
const Home = lazy(() => import('@/pages/Home'))
|
||||
const Project = lazy(() => import('@/pages/Project'))
|
||||
const Session = lazy(() => import('@/pages/Session'))
|
||||
const Settings = lazy(() => import('@/pages/Settings'))
|
||||
const Analytics = lazy(() => import('@/pages/Analytics'))
|
||||
const Results = lazy(() => import('@/pages/Results'))
|
||||
|
||||
// Single Suspense boundary wrapping all routes
|
||||
<Routes>
|
||||
<Route path="/" element={<Suspense fallback={<FullPageLoader />}><Home /></Suspense>} />
|
||||
...
|
||||
</Routes>
|
||||
```
|
||||
|
||||
### Chunk strategy
|
||||
|
||||
- **Vendor chunk**: `react`, `react-dom`, `react-router-dom` → stable, infrequently changed
|
||||
- **UI chunk**: `framer-motion`, `lucide-react`, `react-markdown`, `shiki` → large UI libs
|
||||
- **Per-page chunks**: Each page component loads on demand
|
||||
|
||||
Configured via `vite.config.ts` `build.rollupOptions.output.manualChunks`.
|
||||
|
||||
## 9. Empty State Pattern
|
||||
|
||||
### Standard template
|
||||
|
||||
```tsx
|
||||
// New reusable component
|
||||
function EmptyState({ icon, title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-16">
|
||||
<div className="text-muted-foreground/40">{icon}</div>
|
||||
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground text-center max-w-[240px]">{description}</p>
|
||||
)}
|
||||
{action && <Button variant="outline" size="sm" onClick={action.onClick}>{action.label}</Button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Replace current text-only empty states in `Home.tsx` (no projects), `Project.tsx` (no sessions), and `SessionLandingPage.tsx` (no chats).
|
||||
|
||||
## 10. StatusDot Animation Fix
|
||||
|
||||
### Current
|
||||
```tsx
|
||||
// No transform-origin set — rotation around center creates blur
|
||||
<span className="absolute top-0 left-1/2 -translate-x-1/2 size-0.5 rounded-full bg-amber-500" />
|
||||
```
|
||||
|
||||
### Target
|
||||
```tsx
|
||||
// Explicit transform-origin + motion-safe guard
|
||||
<span className="absolute top-0 left-1/2 -translate-x-1/2 size-0.5 rounded-full bg-amber-500 motion-safe:animate-spin-slow motion-reduce:hidden" style={{ transformOrigin: 'center 6px' }} />
|
||||
```
|
||||
|
||||
The `size-0.5` dots orbit at 12px diameter. `transform-origin: center 6px` centers rotation at the orbit midpoint.
|
||||
78
openspec/changes/impeccable-wave/proposal.md
Normal file
78
openspec/changes/impeccable-wave/proposal.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# The Data Terminal — Animation, Polish & Perceptual Design Wave
|
||||
|
||||
## Why
|
||||
|
||||
BooCode's frontend is functionally complete but perceptually unfinished. The gap analysis against `impeccable`-level quality identified 8 critical gaps holding back the "cutting edge, modern, bold" identity you want:
|
||||
|
||||
1. **No layout animations** — The workspace pane grid (core UX) has zero transitions. Panes snap open/closed/reordered. The interface feels static where it should feel alive.
|
||||
2. **No JS animation library** — Pure CSS only. No springs, no stagger entrances, no shared-element transitions, no interruptible motion. The "Data Terminal" should hum and respond.
|
||||
3. **Messages teleport in** — No stagger entrance on the message list. Every message appears instantly, robbing the conversation of rhythm.
|
||||
4. **Motion timing is ad-hoc** — 22 files hardcode durations independently. No shared easing curves, no timing tokens.
|
||||
5. **No keyboard shortcut reference** — Comprehensive shortcuts exist (C, T, W, Tab, 1-9, backtick) but are invisible. Help/Docs scored 2/40 in the critique.
|
||||
6. **Font choice is safe** — Inter is the shadcn-default AI slop tell. A bolder pairing would elevate the entire identity.
|
||||
7. **No route-level code splitting** — Entire app loads in one bundle. Per Vite best practices, this is CRITICAL.
|
||||
8. **No undo system** — Destructive actions confirm but don't offer recovery. Undo toast is standard UX.
|
||||
9. **Empty states are text-only** — "No projects" / "No sessions" with no illustration or guidance.
|
||||
|
||||
This batch addresses all of these in a structured, layered approach — from foundational motion tokens through to perceptual polish.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Motion is telemetry, not decoration.** Every animation communicates state. If it doesn't serve understanding, it doesn't exist.
|
||||
2. **The Data Terminal should feel alive.** The workspace, messages, and tool calls should have presence — appearing, responding, transitioning with purpose.
|
||||
3. **Cutting edge does not mean gratuitous.** Animations serve the power user. Fast, interruptible, under 300ms. No bounce, no elastic, no choreographed page-load sequences.
|
||||
4. **Accessibility is not optional.** Every animation has a `prefers-reduced-motion` alternative. Every interactive element is keyboard-accessible. Empty states teach.
|
||||
5. **Consistency over surprise.** Motion tokens, easing curves, and timing are shared across every component. Same feel, everywhere.
|
||||
|
||||
## What Changes
|
||||
|
||||
### Phase 1 — Foundation (motion tokens + animation library)
|
||||
|
||||
Install `framer-motion`, define motion design tokens (`--duration-*`, `--ease-*`, `--motion-*`) in `globals.css`, replace all hardcoded timing values across components with the token system. This is the prerequisite for everything else.
|
||||
|
||||
### Phase 2 — Workspace (pane grid animation)
|
||||
|
||||
Animate the multi-pane workspace: pane open/close transitions (slide + fade), pane reorder via layout animation, tab switch transitions. This is the core UX — the single most visible animation gap.
|
||||
|
||||
### Phase 3 — Chat (message animation + stagger)
|
||||
|
||||
Add stagger entrance to the message list. Messages appear in sequence (30ms apart, max 300ms total). New messages scroll-anchor with a subtle entrance. Tool calls expand with spring physics. Reasoning block opens with a smooth height transition.
|
||||
|
||||
### Phase 4 — Visual identity (typography + empty states)
|
||||
|
||||
Swap Inter for a bolder, more distinctive font pairing (e.g. Public Sans + JetBrains Mono, or Instrument Sans + JetBrains Mono — something unexpected but technical). Replace text-only empty states with illustrated CTAs. Add noise/grain texture layer to the background for atmosphere.
|
||||
|
||||
### Phase 5 — UX depth (undo, keyboard reference, code splitting)
|
||||
|
||||
Add undo toasts for destructive actions. Build a keyboard shortcut reference dialog (`Cmd+/` or `?`). Add route-level `React.lazy()` code splitting with `Suspense` boundaries.
|
||||
|
||||
### Phase 6 — Polish pass (critique close-out)
|
||||
|
||||
Fix the P0-P3 issues from the impeccable critique: rounding consistency (done), "tap" label removal, bubble side-stripe, StatusDot transform-origin, ChatInput toolbar density, CompactCard popover overflow. Run a follow-up critique to verify score improvement.
|
||||
|
||||
## New Capabilities
|
||||
|
||||
- `motion-token-system`: CSS custom properties for durations, easing curves, and motion presets in `globals.css`. Every component references tokens, not hardcoded values.
|
||||
- `pane-layout-animations`: Workspace pans animate open/close/reorder via framer-motion `AnimatePresence` + `layout` prop.
|
||||
- `message-stagger-entrance`: Message list items stagger in at 30ms intervals on first render and on new messages.
|
||||
- `keyboard-shortcuts-dialog`: `Cmd+/` panel listing all keyboard shortcuts, grouped by category (navigation, chat, terminal, general).
|
||||
- `undo-toast-system`: Sonner toast with "Undo" action for destructive operations (delete chat, archive project, close pane).
|
||||
- `route-code-splitting`: All 6 page components wrapped in `React.lazy(() => import(...))` with Suspense fallbacks.
|
||||
- `bold-font-pairing`: Replace Inter with a distinctive sans (e.g. Instrument Sans, Public Sans, or Satoshi) while keeping JetBrains Mono for code.
|
||||
- `illustrated-empty-states`: New user/project/session empty states with inline SVG illustrations + CTA buttons.
|
||||
|
||||
## Modified Capabilities
|
||||
|
||||
- `ChatInput-toolbar`: Secondary controls (Flows, SlashCommands, WebSearch) moved to overflow menu. Toolbar density reduced from 7→4 items.
|
||||
- `ToolCallGroup`: "tap" label removed — chevron rotation is sufficient.
|
||||
- `MessageBubble-actions`: Share popover uses portal-based positioning to prevent viewport overflow.
|
||||
- `StatusDot`: `transform-origin: center` added to streaming spinner + `prefers-reduced-motion` guard.
|
||||
- `ember-user-bubble`: Side-stripe accent replaced with solid top-border accent for design system compliance.
|
||||
- `Dynamic-imports`: App.tsx routes switch from static imports to `React.lazy()` with Suspense boundaries.
|
||||
|
||||
## Impact
|
||||
|
||||
- **apps/web**: 20–30 files modified across components, styles, and pages. New files: `lib/motion.ts` (token constants + helpers), `components/KeyboardShortcutsDialog.tsx`, `components/UndoToast.tsx`, empty state SVG illustrations.
|
||||
- **dependencies**: `framer-motion` added to `apps/web/package.json`.
|
||||
- **No server/coder/schema changes**: Purely a frontend batch. No API changes, no new endpoints, no DB migrations.
|
||||
- **Risk areas**: framer-motion bundle size (~30KB gzipped), animation performance on low-end devices, `prefers-reduced-motion` coverage completeness.
|
||||
340
openspec/changes/impeccable-wave/tasks.md
Normal file
340
openspec/changes/impeccable-wave/tasks.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# Implementation Tasks — Impeccable Wave
|
||||
|
||||
**Dependency map:** Tasks within a phase are sequential (foundation before use). Phases 2–5 can run in parallel after Phase 1 completes. Phase 6 (polish) is last.
|
||||
|
||||
```
|
||||
Phase 1 (Foundation)
|
||||
├── 1.1 Motion tokens
|
||||
├── 1.2 Install framer-motion
|
||||
└── 1.3 Create lib/motion.ts
|
||||
│
|
||||
├──► Phase 2 (Workspace) ─── 2.1–2.4
|
||||
├──► Phase 3 (Chat) ──────── 3.1–3.4
|
||||
├──► Phase 4 (Identity) ──── 4.1–4.3
|
||||
└──► Phase 5 (UX Depth) ──── 5.1–5.5
|
||||
│
|
||||
└──► Phase 6 (Polish) ─── 6.1–6.5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Foundation (prerequisite for all visual animation)
|
||||
|
||||
### 1.1 Define motion design tokens in globals.css
|
||||
|
||||
**File:** `apps/web/src/styles/globals.css`
|
||||
|
||||
Add `@theme` block with duration, easing, and motion-preset custom properties. Include `motion-safe:` and `motion-reduce:` variants.
|
||||
|
||||
**Checklist:**
|
||||
- [ ] `--duration-instant: 0ms`, `--duration-fast: 100ms`, `--duration-normal: 150ms`, `--duration-slow: 200ms`, `--duration-deliberate: 300ms`
|
||||
- [ ] `--ease-enter`, `--ease-move`, `--ease-hover`, `--ease-emerge`, `--ease-exit` with cubic-bezier values
|
||||
- [ ] `--motion-reduce-transform: none` with `prefers-reduced-motion` override
|
||||
- [ ] Verify no existing hardcoded values conflict
|
||||
|
||||
**Verification:** `lsp_diagnostics` clean, `pnpm -C apps/web build` passes.
|
||||
|
||||
### 1.2 Install framer-motion
|
||||
|
||||
```bash
|
||||
pnpm -C apps/web add framer-motion
|
||||
```
|
||||
|
||||
**Verification:** `pnpm -C apps/web build` exits 0. Bundle size: check `pnpm -C apps/web build --report` for framer-motion contribution.
|
||||
|
||||
### 1.3 Create `lib/motion.ts` — JS motion constants
|
||||
|
||||
**File:** `apps/web/src/lib/motion.ts`
|
||||
|
||||
Export framer-motion `Transition` config objects referencing the CSS tokens as a single source of truth:
|
||||
|
||||
```typescript
|
||||
export const transitions = {
|
||||
fast: { type: 'spring' as const, stiffness: 400, damping: 30 },
|
||||
normal: { type: 'spring' as const, stiffness: 300, damping: 25 },
|
||||
slow: { type: 'spring' as const, stiffness: 200, damping: 20 },
|
||||
enter: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },
|
||||
exit: { duration: 0.1, ease: [0.4, 0, 0.2, 1] },
|
||||
};
|
||||
|
||||
export const variants = {
|
||||
staggerContainer: { animate: { transition: { staggerChildren: 0.03 } } },
|
||||
fadeSlideIn: { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 } },
|
||||
scaleIn: { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 } },
|
||||
};
|
||||
```
|
||||
|
||||
**Verification:** Importable in any component. `lsp_diagnostics` clean.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Workspace Pane Animation (core UX)
|
||||
|
||||
### 2.1 Add layout animation to pane wrapper
|
||||
|
||||
**File:** `apps/web/src/components/Workspace.tsx`
|
||||
|
||||
Wrap each pane in `<motion.div layout>` with enter/exit animations. Use `AnimatePresence` mode="popLayout" around the pane grid. Panes enter with scale+fade from their edge, exit with quick fade.
|
||||
|
||||
**Edge cases:**
|
||||
- First render: no animation for existing panes (use `initial={false}`)
|
||||
- Single pane with no siblings: no layout shift, skip animation
|
||||
- Mobile single-column: simplified animation (no edge-direction, just fade)
|
||||
|
||||
**Verification:** Open tab, close tab, split pane. Observe smooth transitions. Verify `prefers-reduced-motion` disables all animation.
|
||||
|
||||
### 2.2 Animate tab switch in ChatTabBar
|
||||
|
||||
**File:** `apps/web/src/components/ChatTabBar.tsx`
|
||||
|
||||
Add `layout` prop to tab strip items for reorder animation. New tabs appear with `scaleIn` variant.
|
||||
|
||||
**Verification:** Drag-reorder tabs, close tabs, observe smooth reflow.
|
||||
|
||||
### 2.3 Animate pane split
|
||||
|
||||
**File:** `apps/web/src/components/Workspace.tsx` (split handler)
|
||||
|
||||
When splitting a pane, the new pane emerges from the split point with `transform-origin` at the split edge. Uses `scaleIn` variant.
|
||||
|
||||
**Verification:** Split any pane. New pane scales in from correct origin.
|
||||
|
||||
### 2.4 Add pane drag-reorder animation
|
||||
|
||||
**File:** `apps/web/src/components/Workspace.tsx`
|
||||
|
||||
Enable framer-motion's `reorder` group on the pane grid. Replace HTML5 native drag with framer-motion `Reorder.Group` for spring-animated reordering.
|
||||
|
||||
**Verification:** Drag panes to reorder. Motion is spring-based, not hard snap. Reorder is committed on drop.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Chat Animation (messages + tool calls)
|
||||
|
||||
### 3.1 Add stagger entrance to MessageList
|
||||
|
||||
**File:** `apps/web/src/components/MessageList.tsx`
|
||||
|
||||
Wrap the message list in `<motion.div variants={staggerContainer}>`. Each message `<motion.div variants={fadeSlideIn}>` staggers in at 30ms intervals.
|
||||
|
||||
**Edge cases:**
|
||||
- Initial load: All existing messages should NOT animate (use `initial={false}` on the container)
|
||||
- Streaming messages: Only the latest assistant message should animate in when it starts streaming
|
||||
- Scroll-anchored: When user is at bottom, animate. When scrolled up, suppress animation
|
||||
- Tool call groups: Animate as a single unit (not individual tool calls)
|
||||
|
||||
**Verification:** Open a chat with previous messages — no animation on load. Send a new message — it animates in. Scroll up and send — no animation.
|
||||
|
||||
### 3.2 Animate streaming messages (delta updates)
|
||||
|
||||
**File:** `apps/web/src/components/MessageBubble.tsx`
|
||||
|
||||
Add a subtle pulse/glow animation on the message border while `status === 'streaming'`. The glow transitions from ember orange pulse to steady state on completion.
|
||||
|
||||
```tsx
|
||||
<motion.div
|
||||
animate={message.status === 'streaming' ? { borderColor: ['rgba(255,122,24,0)', 'rgba(255,122,24,0.3)', 'rgba(255,122,24,0)'] } : {}}
|
||||
transition={message.status === 'streaming' ? { duration: 1.5, repeat: Infinity } : {}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Edge cases:** Must stop immediately on completion. Must respect reduced motion (no pulse, just steady border).
|
||||
|
||||
**Verification:** Send a message, observe ember pulse. Message completes, pulse stops. Reduce motion enabled — no pulse.
|
||||
|
||||
### 3.3 Add spring animation to tool call expansion
|
||||
|
||||
**File:** `apps/web/src/components/ToolCallLine.tsx`
|
||||
|
||||
Replace the CSS grid-rows collapse with framer-motion `AnimatePresence` + spring height transition. The expanded content slides down from the collapsed header.
|
||||
|
||||
```tsx
|
||||
<AnimatePresence initial={false}>
|
||||
{expanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
**Verification:** Click tool call to expand — smooth spring transition. Click to collapse — quick exit. Multiple rapid clicks — interruptible.
|
||||
|
||||
### 3.4 Animate reasoning block open
|
||||
|
||||
**File:** `apps/web/src/components/MessageBubble.tsx` (ReasoningBlock)
|
||||
|
||||
Same pattern as tool call expansion — spring-based height transition instead of the current grid-rows CSS trick. Reasoning icon rotates (ChevronRight → ChevronDown) with a spring twist.
|
||||
|
||||
**Verification:** Click reasoning block header — smooth expand. Content appears without delay. Collapse — spring exit.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Visual Identity (fonts + empty states + atmosphere)
|
||||
|
||||
### 4.1 Swap font pairing
|
||||
|
||||
**Files:** `apps/web/src/main.tsx`, `apps/web/src/styles/globals.css`
|
||||
|
||||
- Choose new sans font (e.g. Instrument Sans or Public Sans from Google Fonts)
|
||||
- Add `@fontsource-variable/instrument-sans` import in `main.tsx`
|
||||
- Update `--font-sans` in `globals.css` to `"Instrument Sans Variable", ...`
|
||||
- Verify no layout shift during font swap (font-display: swap)
|
||||
|
||||
**Verification:** All UI text renders in new font. Code renders in JetBrains Mono (unchanged). No flash of invisible text (FOIT). No CLS.
|
||||
|
||||
### 4.2 Create reusable EmptyState component
|
||||
|
||||
**File:** `apps/web/src/components/EmptyState.tsx`
|
||||
|
||||
A reusable component as designed in `design.md` §9. Accepts `icon` (React node), `title`, `description?`, `action?`.
|
||||
|
||||
**Edge cases:**
|
||||
- No icon provided — use a default muted circle
|
||||
- Very long description — truncate with `line-clamp-2`
|
||||
- Icon is text node — wrap in decorative container
|
||||
|
||||
### 4.3 Replace text-only empty states with illustrated versions
|
||||
|
||||
**Files:** `apps/web/src/pages/Home.tsx`, `apps/web/src/pages/Project.tsx`, `apps/web/src/components/SessionLandingPage.tsx`
|
||||
|
||||
Replace "No projects" / "No sessions" / "No chats" plain text with `<EmptyState>` components. Use inline SVGs for illustrations (not external assets).
|
||||
|
||||
**Illustrations needed:**
|
||||
- Home empty: Terminal cursor + code bracket icon
|
||||
- Project empty: Folder + glow dot
|
||||
- Session empty: Chat bubble + ember spark
|
||||
|
||||
**Verification:** Navigate to each page with no data. See illustrated empty state. Action buttons work.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — UX Depth (undo, keyboard reference, code splitting)
|
||||
|
||||
### 5.1 Create keyboard shortcut constant + dialog
|
||||
|
||||
- **File (constants):** `apps/web/src/lib/keyboard-shortcuts.ts`
|
||||
- **File (dialog):** `apps/web/src/components/KeyboardShortcutsDialog.tsx`
|
||||
|
||||
Define all shortcuts as typed constants. Dialog renders a modal with grouped sections. Triggered by `Cmd+/` or `?` in `Session.tsx`. Shortcut keys render with `<kbd>` styling.
|
||||
|
||||
**Edge cases:** Mobile — shortcuts dialog shows touch-equivalent actions. macOS vs Windows — show `Cmd` vs `Ctrl` labels.
|
||||
|
||||
**Verification:** Press `Cmd+/` — dialog opens. Esc closes. Keys are accurate. `kbd` elements are keyboard-focusable.
|
||||
|
||||
### 5.2 Add undo for delete chat
|
||||
|
||||
**File:** `apps/web/src/components/MessageBubble.tsx` (delete handler)
|
||||
|
||||
Before calling `api.messages.remove`, snapshot the messages. Wrap the delete call in an undo toast. On undo, restore via existing API. Timeout: 5 seconds.
|
||||
|
||||
### 5.3 Add undo for archive project
|
||||
|
||||
**File:** `apps/web/src/components/ProjectSidebar.tsx` (archive handler)
|
||||
|
||||
Same pattern as delete chat. Archive → toast with Undo → unarchive on click.
|
||||
|
||||
### 5.4 Add route-level code splitting
|
||||
|
||||
**File:** `apps/web/src/App.tsx`
|
||||
|
||||
Replace static `import Home from '@/pages/Home'` with `const Home = lazy(() => import('@/pages/Home'))`. Add `Suspense` boundary per route with a `FullPageLoader` fallback. Configure `manualChunks` in `vite.config.ts`.
|
||||
|
||||
**Checklist:**
|
||||
- [ ] All 6 page components switched to `lazy()`
|
||||
- [ ] `Suspense` with `FullPageLoader` (existing skeleton pattern)
|
||||
- [ ] `vite.config.ts` `build.rollupOptions.output.manualChunks` for vendor + UI chunk
|
||||
- [ ] Verify named exports work (if pages use `export default`)
|
||||
- [ ] Build and check chunk output
|
||||
|
||||
**Verification:** `pnpm -C apps/web build` produces separate chunk files. Initial page load faster (measure via DevTools network tab).
|
||||
|
||||
### 5.5 Add optional suspense for heavy components
|
||||
|
||||
**File:** `apps/web/src/components/MarkdownRenderer.tsx`, `apps/web/src/components/CodeBlock.tsx`
|
||||
|
||||
Wrap heavy rendering (react-markdown, Shiki highlight) in `<Suspense>` with inline skeleton fallback. The Shiki highlighting in `CodeBlock.tsx` is async — add `lazy()` at component level.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Polish Pass (critique close-out)
|
||||
|
||||
### 6.1 Remove "tap" label from ToolCallGroup
|
||||
|
||||
**File:** `apps/web/src/components/ToolCallGroup.tsx`
|
||||
|
||||
Delete or comment out the `<span>tap</span>` label. The chevron rotation already communicates expandability.
|
||||
|
||||
**Verification:** ToolCallGroup no longer shows "tap" text. Chevron still rotates.
|
||||
|
||||
### 6.2 Fix StatusDot spinner origin + reduced-motion guard
|
||||
|
||||
**File:** `apps/web/src/components/StatusDot.tsx`
|
||||
|
||||
- Add `transform-origin: center` to the spinning dots container
|
||||
- Add `motion-reduce:hidden` class to the animated dots
|
||||
- Verify the static fallback is visible (a single amber dot when motion reduced)
|
||||
|
||||
**Verification:** StatusDot spins smoothly (no blur). Enable reduced motion — static dot appears.
|
||||
|
||||
### 6.3 Fix CompactCard share popover overflow
|
||||
|
||||
**File:** `apps/web/src/components/MessageBubble.tsx` (CompactCard share handler)
|
||||
|
||||
Replace `absolute right-0 top-full` with a portal-based dropdown using the existing `Dialog` component or a boundary-aware positioning utility.
|
||||
|
||||
**Verification:** Share popover never renders past viewport edge, even when trigger is at right edge of message list.
|
||||
|
||||
### 6.4 Fix ChatInput toolbar density
|
||||
|
||||
**File:** `apps/web/src/components/ChatInput.tsx`
|
||||
|
||||
Move Flows, SlashCommands, and WebSearch toggle into an overflow menu (three-dot `MoreHorizontal` button). Keep AgentPicker, Attach, ContextMeter, and Send/Stop always visible.
|
||||
|
||||
**Decision:** The overflow menu uses `DropdownMenu` from shadcn (already exists). Desktop shows 4 items always visible + 3 in overflow. Mobile shows same pattern but with larger touch targets.
|
||||
|
||||
**Verification:** ChatInput toolbar shows 4 items. "..." button reveals 3 more. All actions work from both positions.
|
||||
|
||||
### 6.5 Fix ember user bubble side-stripe
|
||||
|
||||
**File:** `apps/web/src/styles/themes/ember.css`
|
||||
|
||||
Replace `border-right: 2px solid var(--primary)` with `border-top: 2px solid var(--primary)` for design system compliance. This preserves the accent distinction for user messages without violating the side-stripe ban.
|
||||
|
||||
**Verification:** User messages show a top accent border instead of right accent stripe. Looks intentional, not like a leftover.
|
||||
|
||||
---
|
||||
|
||||
## Final Verification
|
||||
|
||||
### Build + typecheck
|
||||
```bash
|
||||
pnpm -C apps/server build
|
||||
pnpm -C apps/web build
|
||||
pnpm -C apps/web typecheck
|
||||
```
|
||||
|
||||
### LSP diagnostics
|
||||
```bash
|
||||
lsp_diagnostics(filePath="apps/web/src", extension=".ts")
|
||||
lsp_diagnostics(filePath="apps/web/src", extension=".tsx")
|
||||
```
|
||||
|
||||
### Visual QA
|
||||
1. Open workspace — panes animate in
|
||||
2. Open chat — messages stagger in
|
||||
3. Send message — streaming pulse, new message enters
|
||||
4. Expand tool call — spring animation
|
||||
5. Press Cmd+/ — shortcuts dialog opens
|
||||
6. Delete chat — undo toast appears, restore works
|
||||
7. Reduce motion — all animations stop
|
||||
8. Switch to mobile viewport — no layout breakage
|
||||
|
||||
### Run critique
|
||||
```bash
|
||||
$impeccable critique Session
|
||||
```
|
||||
Expect score improvement from 35/40 to 37+/40.
|
||||
Reference in New Issue
Block a user