apps/desktop/layer/renderer/src/modules/app-layout/LAYOUT_ARCHITECTURE.md
The Follow application uses a sophisticated nested layout system built on React Router v7 with the Outlet component pattern. This architecture provides a flexible, hierarchical layout structure that supports multiple application modes while maintaining clean separation of concerns.
The application's layout system follows a hierarchical structure where each level handles specific responsibilities:
graph TD
A[App.tsx - Root] --> B[AppLayer with Outlet]
B --> C[MainLayout - pages/main/layout.tsx]
C --> D[MainDestopLayout - Primary Desktop Container]
D --> E[Main Content Area with Outlet]
E --> F{Route Type}
F -->|Timeline Routes| G[TimelineEntryTwoColumnLayout]
F -->|AI-Enhanced Timeline| G2[AIEnhancedTimelineLayout]
F -->|AI Routes| H[AIChatLayout]
F -->|Subview Routes| I[SubviewLayout]
G --> J[EntryColumn + Outlet for Content]
G2 --> J2[EntryColumn + AI Panel + Animated Content]
I --> K[Full-screen Modal with Outlet]
H --> L[AI Panel + Content]
App.tsx)Location: src/App.tsx
Purpose: Root application wrapper
Outlet Usage: <Outlet /> renders the main layout tree
// App.tsx structure
<RootProviders>
<Titlebar /> // Electron only
<AppLayer>
<Outlet /> // Renders main layout based on routes
</AppLayer>
</RootProviders>
MainDestopLayout.tsx)Location: src/modules/app-layout/MainDestopLayout.tsx
Purpose: Primary desktop application layout
Key Features:
// MainDestopLayout structure
<RootContainer>
<EntriesProvider>
<SubscriptionColumnContainer /> // Left sidebar
<main>
<AppErrorBoundary>
<Outlet /> // Renders timeline/AI/subview layouts
</AppErrorBoundary>
</main>
</EntriesProvider>
<GlobalPanels />
</RootContainer>
TimelineColumnLayout.tsx)Location: src/modules/app-layout/timeline-column/TimelineColumnLayout.tsx
Purpose: Two-column resizable layout for feed content
Key Features:
// TimelineEntryTwoColumnLayout structure
<div className="flex min-w-0 grow">
<div style={{width: position}}>
<EntryColumn /> // Left: Entry list
</div>
<PanelSplitter /> // Resizable divider
<Outlet /> // Right: Entry content
</div>
SubviewLayout.tsx)Location: src/modules/app-layout/subview/SubviewLayout.tsx
Purpose: Full-screen modal layout for discovery and utility pages
Key Features:
// SubviewLayout structure
<Focusable>
<div className="relative flex size-full">
<FloatingHeader /> // Glass header with back button
<ScrollArea>
<Outlet /> // Full-screen content (Discover, Power, etc.)
</ScrollArea>
<ProgressFAB /> // Scroll progress indicator
</div>
</Focusable>
AIEnhancedTimelineLayout.tsx)Location: src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx
Purpose: Advanced timeline layout with integrated AI chat functionality
Key Features:
// AIEnhancedTimelineLayout structure
<div className="relative flex min-w-0 grow">
<div className="h-full flex-1">
<EntryColumn /> // Entry list - always visible
<AnimatedOverlays>
<AIEntryHeader /> // Animated header overlay
<EntryContent /> // Animated content overlay
</AnimatedOverlays>
</div>
<AIChatPanel /> // Optional resizable AI panel
<SubscriptionToggler /> // Dynamic subscription control
</div>
AIChatLayout.tsx)Location: src/modules/app-layout/ai/AIChatLayout.tsx
Purpose: Dynamic AI chat interface layout
Key Features:
The routing system connects URLs to specific layouts through the generated routes configuration:
// From generated-routes.ts
export const routes: RouteObject[] = [
{
path: "", // Root path
lazy: () => import("./pages/(main)/layout"), // MainLayout wrapper
children: [
{
path: "",
Component: MainIndex, // Default timeline view
},
{
path: "timeline/:timelineId/:feedId",
lazy: () => import("./pages/(main)/(layer)/timeline/[timelineId]/[feedId]/layout"),
// ^ This loads AIEnhancedTimelineLayout (with AI feature) or TimelineEntryTwoColumnLayout (fallback)
children: [
{
path: ":entryId",
lazy: () =>
import("./pages/(main)/(layer)/timeline/[timelineId]/[feedId]/[entryId]/index"),
// ^ Entry content rendered in AIEnhancedTimelineLayout's animated overlay or TimelineEntryTwoColumnLayout's Outlet
},
],
},
{
path: "",
lazy: () => import("./pages/(main)/(layer)/(subview)/layout"),
// ^ This loads SubviewLayout
children: [
{
path: "discover",
lazy: () => import("./pages/(main)/(layer)/(subview)/discover/index"),
// ^ Discover page rendered in SubviewLayout's Outlet
},
],
},
{
path: "ai",
lazy: () => import("./pages/(main)/(layer)/(ai)/ai/index"),
// ^ AI page rendered in MainDestopLayout's Outlet
},
],
},
]
URL: /timeline/1/feed-123/entry-456
App (Outlet)
→ MainLayout (responsive wrapper)
→ MainDestopLayout (desktop container)
→ AIEnhancedTimelineLayout (AI-enhanced timeline)
→ EntryColumn (base) + EntryContent (animated overlay) + AIPanel (optional)
URL: /discover
App (Outlet)
→ MainLayout (responsive wrapper)
→ MainDestopLayout (desktop container)
→ SubviewLayout (full-screen modal)
→ DiscoverPage (via Outlet)
URL: /ai
App (Outlet)
→ MainLayout (responsive wrapper)
→ MainDestopLayout (desktop container)
→ AIChatLayout (dynamic panel)
Each layout level uses <Outlet /> to render child routes, creating a flexible composition system:
// Parent Layout
function ParentLayout() {
return (
<div className="layout-container">
<Navigation />
<main>
<Outlet /> // Child layouts/pages render here
</main>
</div>
)
}
Layouts adapt based on route parameters and user preferences:
// TimelineEntryTwoColumnLayout
const inWideMode = views.find(v => v.view === view)?.wideMode || false
return (
<div className="flex">
<EntryColumn />
{!inWideMode && <PanelSplitter />}
<Outlet />
</div>
)
Layouts provide context to their children:
// MainDestopLayout
<EntriesProvider>
<SubscriptionColumnContainer />
<main>
<AppErrorBoundary>
<Outlet /> // Child components can access EntriesContext
</AppErrorBoundary>
</main>
</EntriesProvider>
useUISettingKey("feedColWidth")useUISettingKey("entryColWidth")useAIChatPanelStyle()MainDestopLayout)withResponsiveComponent)export const Component = withResponsiveComponent(
() => Promise.resolve({ default: MainDestopLayout }),
async () => {
const { default: MobileLayout } = await import("~/modules/mobile")
return { default: MobileLayout }
},
)
// MainDestopLayout
const errorTypes = [
ErrorComponentType.Page,
ErrorComponentType.FeedFoundCanBeFollow,
ErrorComponentType.FeedNotFound,
] as ErrorComponentType[]
<AppErrorBoundary errorType={errorTypes}>
<Outlet />
</AppErrorBoundary>
All major layouts use React Router's lazy loading:
const lazy16 = () => import("./pages/(main)/layout")
Critical layout dimensions are memoized:
const entryColWidth = useMemo(() => getUISettings().entryColWidth, [])
Scroll and resize handlers use passive listeners and debouncing:
$scroll.addEventListener("scroll", handler, { passive: true })
src/modules/app-layout/src/pages//**
* LayoutName Component
*
* Brief description of layout purpose and features.
*
* Layout Structure:
* ```
* LayoutName
* ├── Header/Navigation
* ├── Content Area
* │ └── Outlet (renders child routes)
* └── Footer/Controls
* ```
*
* @component
* @example
* // Usage context and route examples
*/
export function LayoutName() {
return (
<div className="layout-container">
<Navigation />
<main>
<Outlet />
</main>
</div>
)
}
The Follow app's layout architecture demonstrates sophisticated use of React Router v7's nested routing capabilities. The hierarchical Outlet system creates a flexible, maintainable structure that supports multiple application modes while maintaining clean separation of concerns. Each layout level handles specific responsibilities, from global app state (MainDestopLayout) to specialized interfaces (TimelineEntryTwoColumnLayout, SubviewLayout), creating a scalable foundation for the application's diverse content types and user interfaces.