Skip to content

Commit 4749033

Browse files
committed
feat(web): add AppShell layout with topbar and grain overlay
Shared layout wrapping all protected routes: 56px sticky topbar (logo, live dot, user avatar, sign out) + max-width centered content + warm grain overlay. Landing and auth callback remain full-bleed.
1 parent f47c1ea commit 4749033

4 files changed

Lines changed: 72 additions & 6 deletions

File tree

apps/web/src/app.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router'
22
import { useAuthStore } from './features/auth/store'
33
import OAuthCallback from './features/auth/OAuthCallback'
44
import ProtectedRoute from './features/auth/ProtectedRoute'
5+
import { AppShell } from './shared/components'
56
import InstallationList from './features/dashboard/InstallationList'
67
import InstallationDetail from './features/dashboard/InstallationDetail'
78
import SessionReplay from './features/dashboard/SessionReplay'
@@ -20,14 +21,17 @@ export default function App() {
2021
return (
2122
<BrowserRouter>
2223
<Routes>
24+
{/* Public routes — no shell */}
2325
<Route path="/" element={<AuthRedirect />} />
2426
<Route path="/auth/callback" element={<OAuthCallback />} />
25-
<Route path="/installations" element={<ProtectedRoute><InstallationList /></ProtectedRoute>} />
26-
<Route path="/installations/:installationId" element={<ProtectedRoute><InstallationDetail /></ProtectedRoute>} />
27-
<Route path="/installations/:installationId/sessions/:sessionId" element={<ProtectedRoute><SessionReplay /></ProtectedRoute>} />
28-
<Route path="/installations/:installationId/setup" element={<ProtectedRoute><SetupView /></ProtectedRoute>} />
29-
<Route path="/installations/:installationId/settings" element={<ProtectedRoute><SettingsView /></ProtectedRoute>} />
30-
<Route path="/session/:installationId/*" element={<ProtectedRoute><SessionView /></ProtectedRoute>} />
27+
28+
{/* Protected routes — wrapped in AppShell */}
29+
<Route path="/installations" element={<ProtectedRoute><AppShell><InstallationList /></AppShell></ProtectedRoute>} />
30+
<Route path="/installations/:installationId" element={<ProtectedRoute><AppShell><InstallationDetail /></AppShell></ProtectedRoute>} />
31+
<Route path="/installations/:installationId/sessions/:sessionId" element={<ProtectedRoute><AppShell><SessionReplay /></AppShell></ProtectedRoute>} />
32+
<Route path="/installations/:installationId/setup" element={<ProtectedRoute><AppShell><SetupView /></AppShell></ProtectedRoute>} />
33+
<Route path="/installations/:installationId/settings" element={<ProtectedRoute><AppShell><SettingsView /></AppShell></ProtectedRoute>} />
34+
<Route path="/session/:installationId/*" element={<ProtectedRoute><AppShell><SessionView /></AppShell></ProtectedRoute>} />
3135
</Routes>
3236
</BrowserRouter>
3337
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { GrainOverlay } from './GrainOverlay'
2+
import { Topbar } from './Topbar'
3+
4+
interface AppShellProps {
5+
children: React.ReactNode
6+
}
7+
8+
export function AppShell({ children }: AppShellProps) {
9+
return (
10+
<div className="relative min-h-screen bg-bg text-ink">
11+
<GrainOverlay />
12+
<Topbar />
13+
<main className="relative max-w-[1120px] mx-auto px-5 py-8">
14+
{children}
15+
</main>
16+
</div>
17+
)
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useAuthStore } from '../../features/auth/store'
2+
import { Dot } from './Dot'
3+
4+
export function Topbar() {
5+
const user = useAuthStore((s) => s.user)
6+
const logout = useAuthStore((s) => s.logout)
7+
8+
return (
9+
<header className="sticky top-0 z-50 h-14 bg-bg2 border-b border-rule flex items-center px-5">
10+
<a href="/installations" className="font-mono text-sm font-bold text-accent tracking-tight">
11+
helPRs
12+
</a>
13+
14+
<div className="flex-1" />
15+
16+
{user && (
17+
<div className="flex items-center gap-3">
18+
<Dot color="ok" pulse />
19+
<span className="font-mono text-[11px] text-dim">live</span>
20+
21+
<span className="text-rule-str mx-1">|</span>
22+
23+
{user.avatar_url && (
24+
<img
25+
src={user.avatar_url}
26+
alt={user.github_login}
27+
className="w-6 h-6 rounded-full border border-rule"
28+
/>
29+
)}
30+
<span className="font-mono text-xs text-ink2">{user.github_login}</span>
31+
32+
<button
33+
onClick={logout}
34+
className="font-mono text-[11px] text-dim hover:text-ink2 transition-colors cursor-pointer ml-1"
35+
>
36+
sign out
37+
</button>
38+
</div>
39+
)}
40+
</header>
41+
)
42+
}

apps/web/src/shared/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { AppShell } from './AppShell'
12
export { Button } from './Button'
23
export { Card } from './Card'
34
export { Chip } from './Chip'
@@ -6,3 +7,4 @@ export { GrainOverlay } from './GrainOverlay'
67
export { Overline } from './Overline'
78
export { StatCard } from './StatCard'
89
export { TerminalBlock } from './TerminalBlock'
10+
export { Topbar } from './Topbar'

0 commit comments

Comments
 (0)