Enhance sidebar navigation with icons and mobile support#24
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the Forge FE app shell to replace the top navigation with a sidebar-based layout that includes per-route icons and a responsive mobile drawer experience.
Changes:
- Add icon-backed sidebar navigation items for primary routes.
- Implement a responsive layout with a fixed desktop sidebar and a mobile slide-in drawer + overlay.
- Refactor nav link rendering into a reusable
SidebarLinkcomponent (with badge support).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {/* Mobile overlay */} | ||
| {sidebarOpen && ( | ||
| <div | ||
| className="fixed inset-0 z-40 bg-black/50 lg:hidden animate-in fade-in duration-200" | ||
| onClick={closeSidebar} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
When the mobile sidebar is open, background scrolling isn’t prevented. On touch devices this often causes the main content to scroll behind the overlay. Consider toggling document.body.style.overflow = 'hidden' (and restoring it) while sidebarOpen is true, or using an existing modal/sheet primitive that handles scroll lock.
| function onKeyDown(e: KeyboardEvent) { | ||
| if (e.key === 'Escape') setSidebarOpen(false) | ||
| } | ||
| document.addEventListener('keydown', onKeyDown) | ||
| return () => document.removeEventListener('keydown', onKeyDown) | ||
| }, []) |
There was a problem hiding this comment.
The Escape key listener is registered for the lifetime of the AppShell, even when the sidebar is closed. Consider only attaching the listener while sidebarOpen is true to reduce global event handling and avoid potential interaction with other Escape-driven UI (dialogs, menus).
| function onKeyDown(e: KeyboardEvent) { | |
| if (e.key === 'Escape') setSidebarOpen(false) | |
| } | |
| document.addEventListener('keydown', onKeyDown) | |
| return () => document.removeEventListener('keydown', onKeyDown) | |
| }, []) | |
| if (!sidebarOpen) return | |
| function onKeyDown(e: KeyboardEvent) { | |
| if (e.key === 'Escape') setSidebarOpen(false) | |
| } | |
| document.addEventListener('keydown', onKeyDown) | |
| return () => document.removeEventListener('keydown', onKeyDown) | |
| }, [sidebarOpen]) |
| <button | ||
| onClick={() => setSidebarOpen(true)} | ||
| className="rounded-lg p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| > | ||
| <Menu className="size-5" /> | ||
| </button> |
There was a problem hiding this comment.
Icon-only buttons (mobile menu toggle and close drawer) are missing an accessible name. Please add an aria-label (and/or title) so screen readers can identify the action (e.g., “Open navigation” / “Close navigation”).
| </div> | ||
| <button | ||
| onClick={closeSidebar} | ||
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" |
There was a problem hiding this comment.
The close button in the mobile drawer is icon-only and currently has no accessible name. Add aria-label (and/or title) to ensure it’s discoverable to assistive tech.
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" | |
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" | |
| aria-label="Close navigation menu" | |
| title="Close navigation menu" |
| <aside | ||
| className={cn( | ||
| 'fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-sidebar border-r border-sidebar-border', | ||
| 'transition-transform duration-300 ease-in-out lg:hidden', | ||
| sidebarOpen ? 'translate-x-0' : '-translate-x-full', | ||
| )} | ||
| > | ||
| {/* Drawer header */} | ||
| <div className="flex h-14 items-center justify-between px-5"> | ||
| <div className="flex items-center gap-2.5"> | ||
| <Anvil className="size-5 text-sidebar-primary" /> | ||
| <span className="text-lg font-semibold text-sidebar-foreground"> | ||
| Forge | ||
| </span> | ||
| </div> | ||
| <button | ||
| onClick={closeSidebar} | ||
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" | ||
| > | ||
| <X className="size-5" /> | ||
| </button> | ||
| </div> | ||
| <div className="px-4 pb-3"> | ||
| <ProjectSwitcher /> | ||
| </div> | ||
| <div className="mx-3 border-t border-sidebar-border" /> | ||
| {sidebarContent} | ||
| </aside> |
There was a problem hiding this comment.
The mobile sidebar drawer is always mounted and only moved off-screen via translate-x-*. When closed, its links/buttons can still be reachable via keyboard navigation and some assistive tech. Consider conditionally rendering it only when sidebarOpen, or apply inert/aria-hidden/tabIndex management (or a Dialog/Sheet component) so hidden content isn’t focusable.
| <aside | |
| className={cn( | |
| 'fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-sidebar border-r border-sidebar-border', | |
| 'transition-transform duration-300 ease-in-out lg:hidden', | |
| sidebarOpen ? 'translate-x-0' : '-translate-x-full', | |
| )} | |
| > | |
| {/* Drawer header */} | |
| <div className="flex h-14 items-center justify-between px-5"> | |
| <div className="flex items-center gap-2.5"> | |
| <Anvil className="size-5 text-sidebar-primary" /> | |
| <span className="text-lg font-semibold text-sidebar-foreground"> | |
| Forge | |
| </span> | |
| </div> | |
| <button | |
| onClick={closeSidebar} | |
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" | |
| > | |
| <X className="size-5" /> | |
| </button> | |
| </div> | |
| <div className="px-4 pb-3"> | |
| <ProjectSwitcher /> | |
| </div> | |
| <div className="mx-3 border-t border-sidebar-border" /> | |
| {sidebarContent} | |
| </aside> | |
| {sidebarOpen && ( | |
| <aside | |
| className={cn( | |
| 'fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-sidebar border-r border-sidebar-border', | |
| 'transition-transform duration-300 ease-in-out lg:hidden', | |
| sidebarOpen ? 'translate-x-0' : '-translate-x-full', | |
| )} | |
| > | |
| {/* Drawer header */} | |
| <div className="flex h-14 items-center justify-between px-5"> | |
| <div className="flex items-center gap-2.5"> | |
| <Anvil className="size-5 text-sidebar-primary" /> | |
| <span className="text-lg font-semibold text-sidebar-foreground"> | |
| Forge | |
| </span> | |
| </div> | |
| <button | |
| onClick={closeSidebar} | |
| className="rounded-lg p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground" | |
| > | |
| <X className="size-5" /> | |
| </button> | |
| </div> | |
| <div className="px-4 pb-3"> | |
| <ProjectSwitcher /> | |
| </div> | |
| <div className="mx-3 border-t border-sidebar-border" /> | |
| {sidebarContent} | |
| </aside> | |
| )} |
Frontend Code Review (forge-fe/) |
|
test |
|
Deleting test comments - this is a placeholder |
|
Frontend Code Review forge-fe/ - Full Report SUMMARY TABLE: SECURITY: XSS risk in markdown rendering at forge-fe/src/components/chat/markdown-content.tsx lines 36-41. react-markdown has no allowedElements prop restricting what HTML elements can render. Fix: add allowedElements prop or sanitize server-side. ACCESSIBILITY (High Priority):
REACT BEST PRACTICES:
TYPESCRIPT:
COMPONENT ARCHITECTURE:
PERFORMANCE:
POSITIVE FINDINGS:
Review generated by Claude Code |
Improve the sidebar navigation by adding icons to each link and implementing mobile support for better accessibility and user experience. The sidebar now features a responsive design that adapts to different screen sizes.