Replace top navbar with collapsible sidebar#464
Conversation
|
Warning Review limit reached
More reviews will be available in 7 minutes and 47 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR replaces the fixed top navigation with a responsive, collapsible sidebar. It adds AppShell (layout wrapper with mobile top bar and main slot), Sidebar and SidebarContent (responsive sidebar, persisted collapsed state, admin/emergency-access menu population), updates AuthenticatedMain to use AppShell, refactors UnlockSuccess to use a new UnlockSuccessContent component, and adds i18n keys plus a CHANGELOG entry. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
frontend/src/components/SidebarContent.vue (1)
90-97: 💤 Low valueConsider more precise active route detection.
The
startsWithcheck on Line 91 works for the current navigation paths but could incorrectly match future routes with overlapping prefixes. For example, if/app/vault-settingsis added, it would incorrectly be active when viewing/app/vaults.🔍 More precise matching approach
function itemClasses(to: string) { - const active = route.path.startsWith(to); + const active = route.path === to || route.path.startsWith(to + '/'); return [ active ? 'bg-white/10 text-white' : 'text-gray-300 hover:bg-white/5 hover:text-white', 'group flex items-center gap-x-3 rounded-md p-2 text-sm font-medium', props.collapsed ? 'justify-center' : '' ]; }This ensures the route either exactly matches the nav item path OR starts with the path followed by a slash, preventing
/app/vault-settingsfrom matching/app/vaults.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/SidebarContent.vue` around lines 90 - 97, The active-route check in itemClasses is too broad because route.path.startsWith(to) will match overlapping prefixes; update the logic in the itemClasses function to treat a nav item as active only when route.path === to or route.path starts with to + '/' (i.e., exact match or followed by a slash) so paths like '/app/vault-settings' won't match '/app/vaults'; locate the itemClasses function and replace the active computation accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/Sidebar.vue`:
- Line 31: The button in Sidebar.vue uses the Tailwind class
focus:outline-hidden which prevents the 1px transparent outline needed for
forced-colors accessibility; replace focus:outline-hidden with
focus:outline-none on the toggle button element (the button that binds :title
and `@click`="toggleCollapsed") so the alternate focus indicator
(focus:opacity-100) remains while preserving the transparent outline in
forced-colors mode.
- Around line 59-63: Wrap localStorage access in a try-catch to prevent
exceptions from breaking collapse state: when initializing collapsed (the ref
created with localStorage.getItem(COLLAPSE_KEY) === 'true') catch errors and
fall back to a safe default (e.g., false); similarly update toggleCollapsed to
call localStorage.setItem(COLLAPSE_KEY, String(collapsed.value)) inside a
try-catch and ignore or log errors via console.warn so UI still toggles even if
storage is unavailable; ensure you reference COLLAPSE_KEY, collapsed, and
toggleCollapsed when applying the changes.
In `@frontend/src/components/SidebarContent.vue`:
- Line 37: The MenuButton in SidebarContent.vue currently removes the focus
outline (focus:outline-hidden) leaving keyboard users without any visual focus;
update the class binding on the MenuButton (the element with :title="collapsed ?
me.name : undefined") to replace or augment focus:outline-hidden with a visible
focus state such as adding focus:bg-white/5 and focus:text-white (to mirror
hover) or adding a focus ring like focus:ring-2 focus:ring-offset-1
focus:ring-white/30 (and keep focus:ring-offset-background if needed) so
keyboard navigation shows a clear focus indicator.
---
Nitpick comments:
In `@frontend/src/components/SidebarContent.vue`:
- Around line 90-97: The active-route check in itemClasses is too broad because
route.path.startsWith(to) will match overlapping prefixes; update the logic in
the itemClasses function to treat a nav item as active only when route.path ===
to or route.path starts with to + '/' (i.e., exact match or followed by a slash)
so paths like '/app/vault-settings' won't match '/app/vaults'; locate the
itemClasses function and replace the active computation accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f6d15967-9eef-41dc-a322-d2c5d90bcb4b
⛔ Files ignored due to path filters (1)
frontend/public/logo-text.svgis excluded by!**/*.svg
📒 Files selected for processing (9)
CHANGELOG.mdfrontend/src/components/AppShell.vuefrontend/src/components/AuthenticatedMain.vuefrontend/src/components/NavigationBar.vuefrontend/src/components/Sidebar.vuefrontend/src/components/SidebarContent.vuefrontend/src/components/UnlockSuccess.vuefrontend/src/components/UnlockSuccessContent.vuefrontend/src/i18n/en-US.json
💤 Files with no reviewable changes (1)
- frontend/src/components/NavigationBar.vue
| <!-- Static sidebar for desktop --> | ||
| <div class="group relative hidden bg-tertiary2 transition-[width] duration-200 md:flex md:flex-col" :class="collapsed ? 'md:w-16' : 'md:w-64'"> | ||
| <SidebarContent :me="me" :main-nav="mainNav" :admin-nav="adminNav" :is-admin="isAdmin" :profile-dropdown="profileDropdown" :collapsed="collapsed" /> | ||
| <button type="button" class="absolute top-1/2 -right-3 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-tertiary2 text-gray-300 opacity-0 shadow-sm transition-opacity hover:text-white focus:opacity-100 focus:outline-hidden group-hover:opacity-100 group-focus-within:opacity-100" :title="collapsed ? t('nav.sidebar.expand') : t('nav.sidebar.collapse')" @click="toggleCollapsed"> |
There was a problem hiding this comment.
Replace focus:outline-hidden with focus:outline-none for better forced-colors accessibility.
The toggle button has focus:opacity-100 as an alternative visible focus indicator. As per coding guidelines for Tailwind v4, when an alternative focus indicator exists, use outline-none instead of outline-hidden to preserve a 1px transparent outline in forced-colors mode.
♿ Proposed fix
- <button type="button" class="absolute top-1/2 -right-3 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-tertiary2 text-gray-300 opacity-0 shadow-sm transition-opacity hover:text-white focus:opacity-100 focus:outline-hidden group-hover:opacity-100 group-focus-within:opacity-100" :title="collapsed ? t('nav.sidebar.expand') : t('nav.sidebar.collapse')" `@click`="toggleCollapsed">
+ <button type="button" class="absolute top-1/2 -right-3 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-tertiary2 text-gray-300 opacity-0 shadow-sm transition-opacity hover:text-white focus:opacity-100 focus:outline-none group-hover:opacity-100 group-focus-within:opacity-100" :title="collapsed ? t('nav.sidebar.expand') : t('nav.sidebar.collapse')" `@click`="toggleCollapsed">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button type="button" class="absolute top-1/2 -right-3 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-tertiary2 text-gray-300 opacity-0 shadow-sm transition-opacity hover:text-white focus:opacity-100 focus:outline-hidden group-hover:opacity-100 group-focus-within:opacity-100" :title="collapsed ? t('nav.sidebar.expand') : t('nav.sidebar.collapse')" @click="toggleCollapsed"> | |
| <button type="button" class="absolute top-1/2 -right-3 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-tertiary2 text-gray-300 opacity-0 shadow-sm transition-opacity hover:text-white focus:opacity-100 focus:outline-none group-hover:opacity-100 group-focus-within:opacity-100" :title="collapsed ? t('nav.sidebar.expand') : t('nav.sidebar.collapse')" `@click`="toggleCollapsed"> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/Sidebar.vue` at line 31, The button in Sidebar.vue
uses the Tailwind class focus:outline-hidden which prevents the 1px transparent
outline needed for forced-colors accessibility; replace focus:outline-hidden
with focus:outline-none on the toggle button element (the button that binds
:title and `@click`="toggleCollapsed") so the alternate focus indicator
(focus:opacity-100) remains while preserving the transparent outline in
forced-colors mode.
Source: Learnings
| const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === 'true'); | ||
|
|
||
| function toggleCollapsed() { | ||
| collapsed.value = !collapsed.value; | ||
| localStorage.setItem(COLLAPSE_KEY, String(collapsed.value)); |
There was a problem hiding this comment.
Add error handling for localStorage access.
localStorage.getItem and localStorage.setItem can throw exceptions in private browsing mode, when storage quota is exceeded, or when blocked by browser security policies. The collapse state would fail silently, defaulting to expanded.
🛡️ Proposed fix with try-catch wrapper
-const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === 'true');
+const collapsed = ref(() => {
+ try {
+ return localStorage.getItem(COLLAPSE_KEY) === 'true';
+ } catch {
+ return false;
+ }
+}());
function toggleCollapsed() {
collapsed.value = !collapsed.value;
- localStorage.setItem(COLLAPSE_KEY, String(collapsed.value));
+ try {
+ localStorage.setItem(COLLAPSE_KEY, String(collapsed.value));
+ } catch {
+ // Silently fail if localStorage is unavailable
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === 'true'); | |
| function toggleCollapsed() { | |
| collapsed.value = !collapsed.value; | |
| localStorage.setItem(COLLAPSE_KEY, String(collapsed.value)); | |
| const collapsed = ref(() => { | |
| try { | |
| return localStorage.getItem(COLLAPSE_KEY) === 'true'; | |
| } catch { | |
| return false; | |
| } | |
| }()); | |
| function toggleCollapsed() { | |
| collapsed.value = !collapsed.value; | |
| try { | |
| localStorage.setItem(COLLAPSE_KEY, String(collapsed.value)); | |
| } catch { | |
| // Silently fail if localStorage is unavailable | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/Sidebar.vue` around lines 59 - 63, Wrap localStorage
access in a try-catch to prevent exceptions from breaking collapse state: when
initializing collapsed (the ref created with localStorage.getItem(COLLAPSE_KEY)
=== 'true') catch errors and fall back to a safe default (e.g., false);
similarly update toggleCollapsed to call localStorage.setItem(COLLAPSE_KEY,
String(collapsed.value)) inside a try-catch and ignore or log errors via
console.warn so UI still toggles even if storage is unavailable; ensure you
reference COLLAPSE_KEY, collapsed, and toggleCollapsed when applying the
changes.
| dropdown isn't clipped by the rail's overflow when collapsed --> | ||
| <div class="-mx-2"> | ||
| <Menu as="div" class="relative"> | ||
| <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:outline-hidden', collapsed ? 'justify-center' : '']"> |
There was a problem hiding this comment.
Add visible focus indicator to MenuButton.
The MenuButton has focus:outline-hidden but no alternative visible focus state (only hover states). Keyboard users navigating to this button will see no visual indication of focus, creating an accessibility barrier.
♿ Proposed fix: Add focus state
Option 1: Add focus background/text color (mirrors hover state):
- <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:outline-hidden', collapsed ? 'justify-center' : '']">
+ <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white focus:outline-none', collapsed ? 'justify-center' : '']">Option 2: Add focus ring:
- <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:outline-hidden', collapsed ? 'justify-center' : '']">
+ <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:ring-1 focus:ring-white/20 focus:outline-none', collapsed ? 'justify-center' : '']">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:outline-hidden', collapsed ? 'justify-center' : '']"> | |
| <MenuButton :title="collapsed ? me.name : undefined" :class="['flex w-full items-center gap-x-3 rounded-md p-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white focus:ring-1 focus:ring-white/20 focus:outline-none', collapsed ? 'justify-center' : '']"> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SidebarContent.vue` at line 37, The MenuButton in
SidebarContent.vue currently removes the focus outline (focus:outline-hidden)
leaving keyboard users without any visual focus; update the class binding on the
MenuButton (the element with :title="collapsed ? me.name : undefined") to
replace or augment focus:outline-hidden with a visible focus state such as
adding focus:bg-white/5 and focus:text-white (to mirror hover) or adding a focus
ring like focus:ring-2 focus:ring-offset-1 focus:ring-white/30 (and keep
focus:ring-offset-background if needed) so keyboard navigation shows a clear
focus indicator.
Source: Learnings
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/components/SidebarContent.vue (1)
87-88: AdjuststartsWithactive-state to be boundary-safe (but current routes look safe)
frontend/src/components/SidebarContent.vueusesroute.path.startsWith(to)for active nav state. The currentfrontend/src/router/index.tsroute structure defines distinct/appchild segments (users,groups,vaults,profile,admin,emergency-access) and nested detail routes always continue with/(e.g.,users/:id/edit), so the specific prefix-collision scenario (like/usersvs/users-admin) doesn’t appear in today’s config.For robustness against future sibling paths (or direct navigation to unknown
/app/*), use the boundary-safe form:const active = route.path === to || route.path.startsWith(to + '/');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/SidebarContent.vue` around lines 87 - 88, The active-state check in itemClasses uses route.path.startsWith(to) which can false-positively match prefixes; update itemClasses to compute active using a boundary-safe check (e.g., route.path === to || route.path.startsWith(to + '/')) so only the exact route or its nested children mark active; modify the function itemClasses to replace the current active assignment with this boundary-safe expression and ensure any callers relying on itemClasses keep the same boolean semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/src/components/SidebarContent.vue`:
- Around line 87-88: The active-state check in itemClasses uses
route.path.startsWith(to) which can false-positively match prefixes; update
itemClasses to compute active using a boundary-safe check (e.g., route.path ===
to || route.path.startsWith(to + '/')) so only the exact route or its nested
children mark active; modify the function itemClasses to replace the current
active assignment with this boundary-safe expression and ensure any callers
relying on itemClasses keep the same boolean semantics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4ffd1458-db3c-42e8-960c-4145f794cc44
📒 Files selected for processing (1)
frontend/src/components/SidebarContent.vue
Converts the top navigation bar into a left sidebar.
On desktop it's a full-height sidebar that collapses to an icon-only rail. A chevron on the edge (revealed on hover) toggles it, and the collapsed state persists in localStorage. Administration links (Users, Groups, Audit Logs, Admin) move out of the avatar dropdown into the sidebar under a divider, so navigation lives in one place; the profile menu is pinned to the bottom. On small screens the sidebar becomes an off-canvas drawer behind a hamburger.
The layout is a flexbox app shell (
AppShell.vue) wrappingSidebar+SidebarContent, whichAuthenticatedMainandUnlockSuccessrender through. When collapsed, the rail swaps the full wordmark for the icon-onlylogo.svg.