ui/ UI layer

12 React components. Every component calls usePlanner() to read state — there is no prop drilling anywhere in the app. All styling is inline JSX using var(--token) CSS variables. No CSS modules, no Tailwind.

Parent
Pattern
Each component: const {...} = usePlanner() for state
Styling
Inline style={{}} with var(--token) CSS variables

Children

Component hierarchy

graph TD App --> TP["ThemeProvider"] TP --> PP["PlannerProvider"] PP --> PA["PlannerApp"] PA --> H["Header"] PA --> SR["SemRow ×N"] PA --> SUR["SummerRow ×N"] PA --> BP["BankPanel"] PA --> IP["InfoPanel"] PA --> RL["RelationLines (SVG)"] PA --> DM["DisclaimerModal"] SR --> CC1["CourseCard ×N"] SR --> CB["Co-op block"] SUR --> CC2["CourseCard ×N"] BP --> GBP["Course bank view"] BP --> GP["GradPanel"] GBP --> CC3["CourseCard ×N"] GP --> RN["ReqNode ×N (recursive)"] style PP fill:#fbefff,stroke:#8250df style H fill:#ddf4ff,stroke:#0969da style BP fill:#ddf4ff,stroke:#0969da style GP fill:#ddf4ff,stroke:#0969da

Shared conventions

usePlanner() — the universal hook

Every component starts with a destructuring of usePlanner(). It's a wide import — components pull in exactly what they need. There's never any prop passing for shared state.

Inline styles with CSS variables

// Typical component style pattern
<div style={{
  background: 'var(--bg-surface)',
  border: `1px solid ${isActive ? 'var(--active)' : 'var(--border-2)'}`,
  color: 'var(--text-1)',
  fontSize: isPhone ? 9 : 11,
}}>

isPhone / isMobile responsive flags

Components read isPhone (viewport <600px) and isMobile (<1024px) from context to render different layouts. These flags are computed in PlannerContext from a ResizeObserver.

Drag-drop pattern

Every draggable element calls onDragStart → sets dragInfo in context. Every drop target calls the appropriate context handler (onDrop, onDropBank, etc.). Touch events are handled separately with a ghost element system.