Skip to content

Replace top navbar with sidebar#464

Merged
tobihagemann merged 6 commits into
developfrom
feature/sidebar-navigation
Jun 25, 2026
Merged

Replace top navbar with sidebar#464
tobihagemann merged 6 commits into
developfrom
feature/sidebar-navigation

Conversation

@tobihagemann

@tobihagemann tobihagemann commented Jun 10, 2026

Copy link
Copy Markdown
Member

Converts the top navigation bar into a left sidebar.

On desktop it's a full-height sidebar. 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, with the close button at the top-left so it lines up with the hamburger.

The layout is a flexbox app shell (AppShell.vue) wrapping Sidebar + SidebarContent, which AuthenticatedMain and UnlockSuccess render through.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR replaces the top navigation with a responsive sidebar layout. It adds AppShell, Sidebar, and SidebarContent, updates authenticated layout usage, refactors unlock-state rendering into UnlockSuccessContent, and adds related English translation keys plus a changelog entry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cryptomator/hub#296: Uses auth.hasRole('admin') in navigation menu logic, which is directly related to the role-based menu population in the new sidebar code.

Suggested reviewers

  • overheadhunter
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: replacing the top navbar with a sidebar.
Description check ✅ Passed The description is directly related to the PR and describes the sidebar layout and app shell changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sidebar-navigation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/src/components/SidebarContent.vue (1)

90-97: 💤 Low value

Consider more precise active route detection.

The startsWith check on Line 91 works for the current navigation paths but could incorrectly match future routes with overlapping prefixes. For example, if /app/vault-settings is 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-settings from 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

📥 Commits

Reviewing files that changed from the base of the PR and between fe32d7c and f29022d.

⛔ Files ignored due to path filters (1)
  • frontend/public/logo-text.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • CHANGELOG.md
  • frontend/src/components/AppShell.vue
  • frontend/src/components/AuthenticatedMain.vue
  • frontend/src/components/NavigationBar.vue
  • frontend/src/components/Sidebar.vue
  • frontend/src/components/SidebarContent.vue
  • frontend/src/components/UnlockSuccess.vue
  • frontend/src/components/UnlockSuccessContent.vue
  • frontend/src/i18n/en-US.json
💤 Files with no reviewable changes (1)
  • frontend/src/components/NavigationBar.vue

Comment thread frontend/src/components/Sidebar.vue Outdated
Comment thread frontend/src/components/Sidebar.vue Outdated
Comment thread frontend/src/components/SidebarContent.vue Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/components/SidebarContent.vue (1)

87-88: Adjust startsWith active-state to be boundary-safe (but current routes look safe)

frontend/src/components/SidebarContent.vue uses route.path.startsWith(to) for active nav state. The current frontend/src/router/index.ts route structure defines distinct /app child 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 /users vs /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

📥 Commits

Reviewing files that changed from the base of the PR and between f29022d and 2818835.

📒 Files selected for processing (1)
  • frontend/src/components/SidebarContent.vue

@overheadhunter overheadhunter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From UX tests:

  1. Does the sidebar really need to be collapsible? Isn't this overengineered? No strong veto, just thinking about future difficulties: Whitelabels would need two logo variants as well (given our own logo redesign, the "logo-text.svg" should go anyway).

  2. if collapsing is required, the floating chevron button should be replaced by a permanently visible button that's built-into the sidebar. The hover-effect makes me nervous. And the clickable area is way to small, too. I don't want to play sniper.

  3. on small screens, when the top bar is visible, the position of "open" (burger menu) and "close" (x button) controls should be identical. I don't want to move the cursor if I reconsidered my "open the menu" decision.

Comment thread frontend/src/components/SidebarContent.vue Outdated
Comment thread frontend/src/components/Sidebar.vue

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/Sidebar.vue (1)

67-83: 🩺 Stability & Availability | 🟡 Minor

Wrap the initial sidebar fetches in the same error handling
backend.license.getUserInfo() and backend.settings.get() are awaited outside the try/catch, so a backend error will abort the rest of the mounted setup and skip the emergency-access nav logic. Move them into the guarded block, or catch the whole onMounted body, so the sidebar still renders.

🤖 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 67 - 83, The initial
sidebar data loads in onMounted are not fully protected, so failures from
backend.license.getUserInfo() or backend.settings.get() can abort the rest of
the setup before the emergency-access nav is evaluated. Move those awaits into
the existing try/catch in Sidebar.vue’s onMounted handler, or wrap the entire
mounted body in one guarded block, so the sidebar can continue rendering and
still attempt the recoverable-vault logic even if one backend call fails.
🤖 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.

Outside diff comments:
In `@frontend/src/components/Sidebar.vue`:
- Around line 67-83: The initial sidebar data loads in onMounted are not fully
protected, so failures from backend.license.getUserInfo() or
backend.settings.get() can abort the rest of the setup before the
emergency-access nav is evaluated. Move those awaits into the existing try/catch
in Sidebar.vue’s onMounted handler, or wrap the entire mounted body in one
guarded block, so the sidebar can continue rendering and still attempt the
recoverable-vault logic even if one backend call fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8f427e1-f82d-4e6a-a528-9b60a48b7df6

📥 Commits

Reviewing files that changed from the base of the PR and between e00d719 and b5633c9.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • frontend/src/components/Sidebar.vue
  • frontend/src/components/SidebarContent.vue
  • frontend/src/i18n/en-US.json
💤 Files with no reviewable changes (1)
  • frontend/src/i18n/en-US.json
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

@tobihagemann tobihagemann changed the title Replace top navbar with collapsible sidebar Replace top navbar with sidebar Jun 24, 2026
@tobihagemann
tobihagemann merged commit 4b39853 into develop Jun 25, 2026
9 checks passed
@tobihagemann
tobihagemann deleted the feature/sidebar-navigation branch June 25, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants