Skip to content

Commit 0e0d92e

Browse files
gcmsgclaude
andcommitted
feat: add mobile navigation, toast system, and confirm dialogs
Phase 8 (Mobile Nav): responsive hamburger menus for PublicLayout, ConsoleLayout, and AppLayout with Sheet slide-in panels; collapsible desktop sidebar with localStorage persistence; responsive PlaygroundPage with single-column mobile layout. Phase 9 (Toast + AlertDialog): replace all alert()/confirm() calls with sonner toasts and Radix AlertDialog-based ConfirmDialog. New UI components: Sheet, AlertDialog, ConfirmDialog, Tooltip, useIsMobile hook. i18n keys added across all 8 locales. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 644fe79 commit 0e0d92e

28 files changed

+1211
-177
lines changed

web/app/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"react-i18next": "^16.5.6",
2222
"react-router-dom": "^7.13.1",
2323
"recharts": "^3.7.0",
24+
"sonner": "^2.0.7",
2425
"tailwind-merge": "^3.5.0"
2526
},
2627
"devDependencies": {

web/app/src/App.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Routes, Route } from "react-router-dom"
2+
import { Toaster } from "sonner"
23
import { AppLayout } from "@/components/layout/AppLayout"
34
import { ConsoleLayout } from "@/components/layout/ConsoleLayout"
45
import { PublicLayout } from "@/components/public/PublicLayout"
@@ -96,6 +97,17 @@ export function App() {
9697
{/* Catch-all 404 */}
9798
<Route path="*" element={<NotFoundPage />} />
9899
</Routes>
100+
<Toaster
101+
theme="dark"
102+
position="bottom-right"
103+
toastOptions={{
104+
classNames: {
105+
toast: "bg-card border-border text-foreground",
106+
error: "bg-destructive/10 border-destructive/30 text-destructive",
107+
success: "bg-emerald-500/10 border-emerald-500/30 text-emerald-400",
108+
},
109+
}}
110+
/>
99111
</AuthProvider>
100112
</div>
101113
)

web/app/src/components/layout/AppLayout.tsx

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,85 @@
1-
import { Outlet } from "react-router-dom"
1+
import { useState, useEffect } from "react"
2+
import { Outlet, useLocation } from "react-router-dom"
3+
import { useTranslation } from "react-i18next"
4+
import { useIsMobile } from "@/hooks/use-mobile"
25
import { Sidebar } from "./Sidebar"
6+
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"
7+
import { Menu, PanelLeftClose, PanelLeftOpen } from "lucide-react"
8+
9+
const COLLAPSED_KEY = "peerclaw_admin_sidebar_collapsed"
310

411
export function AppLayout() {
12+
const { t } = useTranslation()
13+
const isMobile = useIsMobile()
14+
const location = useLocation()
15+
const [mobileOpen, setMobileOpen] = useState(false)
16+
const [collapsed, setCollapsed] = useState(() => {
17+
try { return localStorage.getItem(COLLAPSED_KEY) === "true" } catch { return false }
18+
})
19+
20+
// Close mobile sheet on route change
21+
useEffect(() => {
22+
setMobileOpen(false)
23+
}, [location.pathname])
24+
25+
const toggleCollapsed = () => {
26+
setCollapsed((prev) => {
27+
const next = !prev
28+
try { localStorage.setItem(COLLAPSED_KEY, String(next)) } catch {}
29+
return next
30+
})
31+
}
32+
533
return (
634
<div className="flex h-screen overflow-hidden bg-background">
7-
<Sidebar />
8-
<main className="flex-1 overflow-y-auto p-6">
9-
<Outlet />
35+
{/* Desktop sidebar */}
36+
{!isMobile && (
37+
<div className="hidden md:flex">
38+
<Sidebar collapsed={collapsed} />
39+
</div>
40+
)}
41+
42+
{/* Mobile sheet */}
43+
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
44+
<SheetContent side="left" className="w-56 p-0">
45+
<SheetTitle className="sr-only">{t('nav.menu')}</SheetTitle>
46+
<Sidebar />
47+
</SheetContent>
48+
</Sheet>
49+
50+
{/* Main content */}
51+
<main className="flex-1 overflow-y-auto">
52+
{/* Mobile header */}
53+
{isMobile && (
54+
<div className="flex h-12 items-center border-b border-border px-4">
55+
<button
56+
onClick={() => setMobileOpen(true)}
57+
className="mr-2 rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
58+
aria-label={t('nav.menu')}
59+
>
60+
<Menu className="size-5" />
61+
</button>
62+
<div className="flex items-center gap-2 flex-1 min-w-0">
63+
<img src="/logo.jpg" alt="PeerClaw" className="size-6 rounded-md object-cover" />
64+
<span className="font-semibold text-sm truncate">{t('nav.peerclawAdmin')}</span>
65+
</div>
66+
</div>
67+
)}
68+
{/* Desktop collapse toggle */}
69+
{!isMobile && (
70+
<div className="flex h-12 items-center border-b border-border px-6">
71+
<button
72+
onClick={toggleCollapsed}
73+
className="rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
74+
title={collapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar')}
75+
>
76+
{collapsed ? <PanelLeftOpen className="size-4" /> : <PanelLeftClose className="size-4" />}
77+
</button>
78+
</div>
79+
)}
80+
<div className="p-6">
81+
<Outlet />
82+
</div>
1083
</main>
1184
</div>
1285
)

0 commit comments

Comments
 (0)