# NavBuddy2
[](https://hex.pm/packages/nav_buddy2)
**Permission-aware, multi-layout navigation engine for Phoenix LiveView.**
One navigation tree → multiple renderers → full daisyUI theming → Alpine.js animations.
## Features
- **Two-level sidebar** – icon rail + collapsible detail panel (like the React reference)
- **Horizontal navbar** – top navigation bar with dropdown menus
- **Mobile drawer** – slide-out navigation for small screens
- **Command palette** – ⌘K / Ctrl+K searchable overlay
- **Permission-aware** – items hidden from unauthorized users
- **3-level depth** – Sidebar → Section → Item (with optional children)
- **Layout persistence** – users pick sidebar or horizontal (persisted like dark mode)
- **daisyUI themed** – inherits your theme automatically
- **Alpine.js powered** – smooth transitions, no full-page reloads
- **LiveView-native** – uses `phx-click`, `navigate`, and Alpine for UI state
## Installation
Add to `mix.exs`:
```elixir
def deps do
[
{:nav_buddy2, "~> 0.2.0"}
]
end
```
## Quick Setup
### 1. Configure icon renderer
```elixir
# config/config.exs
config :nav_buddy2,
icon_renderer: &MyAppWeb.NavIcon.render/1
```
Example renderer for Heroicons (default in Phoenix 1.7+):
```elixir
defmodule MyAppWeb.NavIcon do
use Phoenix.Component
def render(assigns) do
~H"""
<.icon name={"hero-#{@name}"} class={@class} />
"""
end
end
```
### 2. Define your navigation
```elixir
defmodule MyAppWeb.Navigation do
alias NavBuddy2.{Sidebar, Section, Item}
def sidebars do
[
%Sidebar{
id: :home,
title: "Home",
icon: :home,
position: :top,
sections: [
%Section{
title: "Overview",
items: [
%Item{label: "Dashboard", icon: :squares_2x2, to: "/", exact: true},
%Item{
label: "Projects",
icon: :folder,
children: [
%Item{label: "Active", to: "/projects/active"},
%Item{label: "Archived", to: "/projects/archived"}
]
}
]
}
]
},
%Sidebar{
id: :settings,
title: "Settings",
icon: :cog_6_tooth,
position: :bottom,
permission: :admin,
sections: [
%Section{
title: "Account",
items: [
%Item{label: "Profile", icon: :user, to: "/settings/profile"},
%Item{label: "Security", icon: :shield_check, to: "/settings/security"}
]
}
]
}
]
end
end
```
Or use the DSL builder:
```elixir
NavBuddy2.build(
home: [
title: "Home",
icon: :home,
sections: [
[title: "Overview", items: [
[label: "Dashboard", icon: :squares_2x2, to: "/", exact: true]
]]
]
]
)
```
### 3. Add to your layout
```heex
<NavBuddy2.Nav.nav
sidebars={MyAppWeb.Navigation.sidebars()}
current_user={@current_user}
current_path={@current_path}
>
<main class="flex-1 p-6">
<%= @inner_content %>
</main>
</NavBuddy2.Nav.nav>
```
### 4. Handle events in your LiveView
```elixir
def handle_event("nav_buddy2:switch_sidebar", %{"id" => id}, socket) do
{:noreply, assign(socket, :active_sidebar_id, String.to_existing_atom(id))}
end
```
### 5. JavaScript setup
Install Alpine.js and the persist plugin:
```bash
npm install alpinejs @alpinejs/persist @alpinejs/collapse
```
In your `app.js`:
```javascript
import Alpine from "alpinejs"
import persist from "@alpinejs/persist"
import collapse from "@alpinejs/collapse"
import NavBuddy2Plugin from "nav_buddy2/assets/nav_buddy2"
Alpine.plugin(persist)
Alpine.plugin(collapse)
Alpine.plugin(NavBuddy2Plugin)
window.Alpine = Alpine
Alpine.start()
```
## Permissions
Implement the `NavBuddy2.PermissionResolver` behaviour:
```elixir
defmodule MyApp.NavPermissions do
@behaviour NavBuddy2.PermissionResolver
@impl true
def can?(user, permission) do
permission in user.permissions
end
end
```
Configure it:
```elixir
config :nav_buddy2, permission_resolver: MyApp.NavPermissions
```
Items, sections, and sidebars with a `:permission` field will be hidden from users who lack that permission. If no resolver is configured, everything is visible.
## Layout Switching
Users can switch between sidebar and horizontal layouts at runtime. The preference is persisted in localStorage (like dark/light mode). A small floating toggle appears in the bottom-right corner.
Available layouts:
- `"sidebar"` – Two-level sidebar (icon rail + detail)
- `"horizontal"` – Top navigation bar
- `"auto"` – Sidebar on desktop, horizontal + drawer on mobile
## Component Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `sidebars` | list | required | List of `NavBuddy2.Sidebar` structs |
| `current_user` | any | required | User struct for permission checks |
| `current_path` | string | required | Current route path |
| `layout` | string | `"sidebar"` | Default layout mode |
| `collapsed` | boolean | `false` | Initial sidebar collapsed state |
| `active_sidebar_id` | any | first id | Currently active sidebar |
| `searchable` | boolean | `true` | Show search input |
| `command_palette` | boolean | `true` | Enable ⌘K palette |
| `class` | string | `""` | Root container classes |
| `sidebar_class` | string | `""` | Sidebar panel classes |
| `rail_class` | string | `""` | Icon rail classes |
| `horizontal_class` | string | `""` | Horizontal nav classes |
| `logo` | any | nil | Custom logo content |
## Architecture
```
Navigation Definition (structs)
↓
Permission Resolver (filter tree)
↓
Layout Router (sidebar | horizontal | auto)
↓
Renderer Layer (IconRail, Sidebar, Horizontal, MobileDrawer, CommandPalette)
↓
Client UI (Alpine.js + daisyUI + LiveView)
```
## Compared to NavBuddy v1
| Feature | v1 | v2 |
|---------|----|----|
| daisyUI support | ❌ | ✅ |
| Multiple layouts | ❌ | ✅ sidebar, horizontal, auto |
| Layout persistence | ❌ | ✅ localStorage |
| Command palette | ❌ | ✅ ⌘K |
| Mobile drawer | ❌ | ✅ |
| 3-level depth | ❌ | ✅ |
| Permission support | Basic | ✅ Full (sidebar, section, item) |
| Alpine.js animations | ❌ | ✅ |
| Search | ❌ | ✅ |
| Badges | ❌ | ✅ |
## License
MIT