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.
|
||||
Reference in New Issue
Block a user