Packages
Advanced configurable navigation component for Phoenix LiveView with native DaisyUI integration, Tailwind CSS support, permissions filtering, dropdowns, mega menus, and modern responsive design. Optimized for Phoenix 1.8+ and LiveView 1.1+.
Current section
8 Versions
Jump to
Current section
8 Versions
Compare versions
5
files changed
+178
additions
-83
deletions
| @@ -81,10 +81,12 @@ NavBuddy supports various navigation structures with comprehensive validation an | |
| 81 81 | ### Required vs Optional Fields |
| 82 82 | |
| 83 83 | **Required fields:** |
| 84 | + |
| 84 85 | - `id` (string) - Unique identifier for the navigation item |
| 85 86 | - `label` (string) - Display text for the navigation item |
| 86 87 | |
| 87 88 | **Optional fields:** |
| 89 | + |
| 88 90 | - `navigate` (string) - Path to navigate to (required for clickable items) |
| 89 91 | - `icon` (string) - Icon class or identifier |
| 90 92 | - `dropdown` (list) - List of child navigation items |
| @@ -113,7 +115,7 @@ dropdown_items = [ | |
| 113 115 | ] |
| 114 116 | NavBuddy.dropdown_item("products", "Products", dropdown_items) |
| 115 117 | |
| 116 | - # Mega menu item |
| 118 | + # Mega menu item |
| 117 119 | categories = %{ |
| 118 120 | "Frontend" => [NavBuddy.nav_item("react", "React", "/tools/react")], |
| 119 121 | "Backend" => [NavBuddy.nav_item("elixir", "Elixir", "/tools/elixir")] |
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"Docs">>,<<"https://hexdocs.pm/navbuddy">>}, |
| 3 3 | {<<"GitHub">>,<<"https://github.com/SangRJ/navbuddy">>}]}. |
| 4 4 | {<<"name">>,<<"navbuddy">>}. |
| 5 | - {<<"version">>,<<"0.2.0">>}. |
| 5 | + {<<"version">>,<<"0.2.3">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"Advanced configurable navigation component for Phoenix LiveView applications with permissions-based filtering, comprehensive validation, convenient helper functions, and support for dropdowns, mega menus, and mobile-responsive design.">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -266,16 +266,19 @@ defmodule NavBuddy do | |
| 266 266 | {:error, "Navigation items must be a list, got: #{inspect(items)}"} |
| 267 267 | end |
| 268 268 | |
| 269 | - defp validate_required_fields(%{id: id, label: label}) when is_binary(id) and is_binary(label) do |
| 269 | + defp validate_required_fields(%{id: id, label: label}) |
| 270 | + when is_binary(id) and is_binary(label) do |
| 270 271 | :ok |
| 271 272 | end |
| 272 273 | |
| 273 274 | defp validate_required_fields(%{id: id}) when is_binary(id) do |
| 274 | - {:error, "Navigation item missing required field 'label'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 275 | + {:error, |
| 276 | + "Navigation item missing required field 'label'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 275 277 | end |
| 276 278 | |
| 277 279 | defp validate_required_fields(%{label: label}) when is_binary(label) do |
| 278 | - {:error, "Navigation item missing required field 'id'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 280 | + {:error, |
| 281 | + "Navigation item missing required field 'id'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 279 282 | end |
| 280 283 | |
| 281 284 | defp validate_required_fields(%{id: id}) do |
| @@ -287,7 +290,8 @@ defmodule NavBuddy do | |
| 287 290 | end |
| 288 291 | |
| 289 292 | defp validate_required_fields(_) do |
| 290 | - {:error, "Navigation item missing required fields 'id' and 'label'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 293 | + {:error, |
| 294 | + "Navigation item missing required fields 'id' and 'label'. Required fields: id (string), label (string). Optional fields: navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 291 295 | end |
| 292 296 | |
| 293 297 | defp validate_field_types(item) do |
| @@ -301,55 +305,82 @@ defmodule NavBuddy do | |
| 301 305 | |
| 302 306 | defp validate_field_type(:navigate, value) when is_binary(value), do: :ok |
| 303 307 | defp validate_field_type(:navigate, nil), do: :ok |
| 304 | - defp validate_field_type(:navigate, value), do: {:error, "Field 'navigate' must be a string, got: #{inspect(value)}"} |
| 308 | + |
| 309 | + defp validate_field_type(:navigate, value), |
| 310 | + do: {:error, "Field 'navigate' must be a string, got: #{inspect(value)}"} |
| 305 311 | |
| 306 312 | defp validate_field_type(:icon, value) when is_binary(value), do: :ok |
| 307 313 | defp validate_field_type(:icon, nil), do: :ok |
| 308 | - defp validate_field_type(:icon, value), do: {:error, "Field 'icon' must be a string, got: #{inspect(value)}"} |
| 314 | + |
| 315 | + defp validate_field_type(:icon, value), |
| 316 | + do: {:error, "Field 'icon' must be a string, got: #{inspect(value)}"} |
| 309 317 | |
| 310 318 | defp validate_field_type(:active, value) when is_boolean(value), do: :ok |
| 311 319 | defp validate_field_type(:active, nil), do: :ok |
| 312 | - defp validate_field_type(:active, value), do: {:error, "Field 'active' must be a boolean, got: #{inspect(value)}"} |
| 320 | + |
| 321 | + defp validate_field_type(:active, value), |
| 322 | + do: {:error, "Field 'active' must be a boolean, got: #{inspect(value)}"} |
| 313 323 | |
| 314 324 | defp validate_field_type(:disabled, value) when is_boolean(value), do: :ok |
| 315 325 | defp validate_field_type(:disabled, nil), do: :ok |
| 316 | - defp validate_field_type(:disabled, value), do: {:error, "Field 'disabled' must be a boolean, got: #{inspect(value)}"} |
| 326 | + |
| 327 | + defp validate_field_type(:disabled, value), |
| 328 | + do: {:error, "Field 'disabled' must be a boolean, got: #{inspect(value)}"} |
| 317 329 | |
| 318 330 | defp validate_field_type(:permissions, value) when is_binary(value), do: :ok |
| 331 | + |
| 319 332 | defp validate_field_type(:permissions, value) when is_list(value) do |
| 320 333 | if Enum.all?(value, &is_binary/1) do |
| 321 334 | :ok |
| 322 335 | else |
| 323 | - {:error, "Field 'permissions' when a list must contain only strings, got: #{inspect(value)}"} |
| 336 | + {:error, |
| 337 | + "Field 'permissions' when a list must contain only strings, got: #{inspect(value)}"} |
| 324 338 | end |
| 325 339 | end |
| 340 | + |
| 326 341 | defp validate_field_type(:permissions, nil), do: :ok |
| 327 | - defp validate_field_type(:permissions, value), do: {:error, "Field 'permissions' must be a string or list of strings, got: #{inspect(value)}"} |
| 342 | + |
| 343 | + defp validate_field_type(:permissions, value), |
| 344 | + do: |
| 345 | + {:error, "Field 'permissions' must be a string or list of strings, got: #{inspect(value)}"} |
| 328 346 | |
| 329 347 | defp validate_field_type(:dropdown, value) when is_list(value), do: :ok |
| 330 348 | defp validate_field_type(:dropdown, nil), do: :ok |
| 331 | - defp validate_field_type(:dropdown, value), do: {:error, "Field 'dropdown' must be a list of navigation items, got: #{inspect(value)}"} |
| 349 | + |
| 350 | + defp validate_field_type(:dropdown, value), |
| 351 | + do: {:error, "Field 'dropdown' must be a list of navigation items, got: #{inspect(value)}"} |
| 332 352 | |
| 333 353 | defp validate_field_type(:mega_menu, value) when is_map(value), do: :ok |
| 334 354 | defp validate_field_type(:mega_menu, nil), do: :ok |
| 335 | - defp validate_field_type(:mega_menu, value), do: {:error, "Field 'mega_menu' must be a map, got: #{inspect(value)}"} |
| 355 | + |
| 356 | + defp validate_field_type(:mega_menu, value), |
| 357 | + do: {:error, "Field 'mega_menu' must be a map, got: #{inspect(value)}"} |
| 336 358 | |
| 337 359 | # Allow id and label since they're already validated above |
| 338 360 | defp validate_field_type(:id, _), do: :ok |
| 339 361 | defp validate_field_type(:label, _), do: :ok |
| 340 362 | |
| 341 | - defp validate_field_type(key, value), do: {:error, "Unknown field '#{key}' with value #{inspect(value)}. Allowed fields: id, label, navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 363 | + defp validate_field_type(key, value), |
| 364 | + do: |
| 365 | + {:error, |
| 366 | + "Unknown field '#{key}' with value #{inspect(value)}. Allowed fields: id, label, navigate, icon, dropdown, mega_menu, active, disabled, permissions"} |
| 342 367 | |
| 343 | - defp validate_navigation_logic(%{dropdown: dropdown, mega_menu: mega_menu}) when not is_nil(dropdown) and not is_nil(mega_menu) do |
| 344 | - {:error, "Navigation item cannot have both 'dropdown' and 'mega_menu'. Choose one or the other."} |
| 368 | + defp validate_navigation_logic(%{dropdown: dropdown, mega_menu: mega_menu}) |
| 369 | + when not is_nil(dropdown) and not is_nil(mega_menu) do |
| 370 | + {:error, |
| 371 | + "Navigation item cannot have both 'dropdown' and 'mega_menu'. Choose one or the other."} |
| 345 372 | end |
| 346 373 | |
| 347 | - defp validate_navigation_logic(%{dropdown: dropdown, navigate: navigate}) when not is_nil(dropdown) and not is_nil(navigate) do |
| 348 | - {:error, "Navigation item with 'dropdown' should not have 'navigate' field. Dropdown items should navigate through their child items."} |
| 374 | + defp validate_navigation_logic(%{dropdown: dropdown, navigate: navigate}) |
| 375 | + when not is_nil(dropdown) and not is_nil(navigate) do |
| 376 | + {:error, |
| 377 | + "Navigation item with 'dropdown' should not have 'navigate' field. Dropdown items should navigate through their child items."} |
| 349 378 | end |
| 350 379 | |
| 351 | - defp validate_navigation_logic(%{mega_menu: mega_menu, navigate: navigate}) when not is_nil(mega_menu) and not is_nil(navigate) do |
| 352 | - {:error, "Navigation item with 'mega_menu' should not have 'navigate' field. Mega menu items should navigate through their child items."} |
| 380 | + defp validate_navigation_logic(%{mega_menu: mega_menu, navigate: navigate}) |
| 381 | + when not is_nil(mega_menu) and not is_nil(navigate) do |
| 382 | + {:error, |
| 383 | + "Navigation item with 'mega_menu' should not have 'navigate' field. Mega menu items should navigate through their child items."} |
| 353 384 | end |
| 354 385 | |
| 355 386 | defp validate_navigation_logic(_), do: :ok |
| @@ -363,7 +394,8 @@ defmodule NavBuddy do | |
| 363 394 | end |
| 364 395 | end |
| 365 396 | |
| 366 | - defp validate_dropdown(dropdown), do: {:error, "Dropdown must be a list of navigation items, got: #{inspect(dropdown)}"} |
| 397 | + defp validate_dropdown(dropdown), |
| 398 | + do: {:error, "Dropdown must be a list of navigation items, got: #{inspect(dropdown)}"} |
| 367 399 | |
| 368 400 | defp validate_mega_menu(nil), do: {:ok, nil} |
| 369 401 | |
| @@ -377,18 +409,23 @@ defmodule NavBuddy do | |
| 377 409 | end |
| 378 410 | end |
| 379 411 | |
| 380 | - defp validate_mega_menu(mega_menu), do: {:error, "Mega menu must be a map, got: #{inspect(mega_menu)}"} |
| 412 | + defp validate_mega_menu(mega_menu), |
| 413 | + do: {:error, "Mega menu must be a map, got: #{inspect(mega_menu)}"} |
| 381 414 | |
| 382 415 | defp validate_simple_mega_menu(mega_menu) do |
| 383 416 | Enum.reduce_while(mega_menu, {:ok, mega_menu}, fn {category, items}, {:ok, acc} -> |
| 384 417 | case validate_nav_items(items) do |
| 385 | - {:ok, _} -> {:cont, {:ok, acc}} |
| 386 | - {:error, reason} -> {:halt, {:error, "Invalid items in mega menu category '#{category}': #{reason}"}} |
| 418 | + {:ok, _} -> |
| 419 | + {:cont, {:ok, acc}} |
| 420 | + |
| 421 | + {:error, reason} -> |
| 422 | + {:halt, {:error, "Invalid items in mega menu category '#{category}': #{reason}"}} |
| 387 423 | end |
| 388 424 | end) |
| 389 425 | end |
| 390 426 | |
| 391 | - defp validate_structured_mega_menu(%{title: title, sections: sections}) when is_binary(title) and is_list(sections) do |
| 427 | + defp validate_structured_mega_menu(%{title: title, sections: sections}) |
| 428 | + when is_binary(title) and is_list(sections) do |
| 392 429 | case validate_mega_menu_sections(sections) do |
| 393 430 | {:ok, _} -> {:ok, %{title: title, sections: sections}} |
| 394 431 | {:error, reason} -> {:error, "Invalid mega menu sections: #{reason}"} |
| @@ -396,11 +433,13 @@ defmodule NavBuddy do | |
| 396 433 | end |
| 397 434 | |
| 398 435 | defp validate_structured_mega_menu(%{sections: sections}) when is_list(sections) do |
| 399 | - {:error, "Structured mega menu missing required field 'title'. Required: title (string), sections (list). Optional: description (string)"} |
| 436 | + {:error, |
| 437 | + "Structured mega menu missing required field 'title'. Required: title (string), sections (list). Optional: description (string)"} |
| 400 438 | end |
| 401 439 | |
| 402 440 | defp validate_structured_mega_menu(%{title: title}) when is_binary(title) do |
| 403 | - {:error, "Structured mega menu missing required field 'sections'. Required: title (string), sections (list). Optional: description (string)"} |
| 441 | + {:error, |
| 442 | + "Structured mega menu missing required field 'sections'. Required: title (string), sections (list). Optional: description (string)"} |
| 404 443 | end |
| 405 444 | |
| 406 445 | defp validate_structured_mega_menu(%{title: title}) do |
| @@ -412,7 +451,8 @@ defmodule NavBuddy do | |
| 412 451 | end |
| 413 452 | |
| 414 453 | defp validate_structured_mega_menu(mega_menu) do |
| 415 | - {:error, "Structured mega menu missing required fields. Required: title (string), sections (list). Optional: description (string). Got: #{inspect(mega_menu)}"} |
| 454 | + {:error, |
| 455 | + "Structured mega menu missing required fields. Required: title (string), sections (list). Optional: description (string). Got: #{inspect(mega_menu)}"} |
| 416 456 | end |
| 417 457 | |
| 418 458 | defp validate_mega_menu_sections(sections) do |
| @@ -428,7 +468,8 @@ defmodule NavBuddy do | |
| 428 468 | end |
| 429 469 | end |
| 430 470 | |
| 431 | - defp validate_mega_menu_section(%{title: title, items: items}) when is_binary(title) and is_list(items) do |
| 471 | + defp validate_mega_menu_section(%{title: title, items: items}) |
| 472 | + when is_binary(title) and is_list(items) do |
| 432 473 | case validate_nav_items(items) do |
| 433 474 | {:ok, _} -> {:ok, %{title: title, items: items}} |
| 434 475 | {:error, reason} -> {:error, "Invalid items in mega menu section '#{title}': #{reason}"} |
| @@ -436,11 +477,13 @@ defmodule NavBuddy do | |
| 436 477 | end |
| 437 478 | |
| 438 479 | defp validate_mega_menu_section(%{items: items}) when is_list(items) do |
| 439 | - {:error, "Mega menu section missing required field 'title'. Required: title (string), items (list). Optional: description (string)"} |
| 480 | + {:error, |
| 481 | + "Mega menu section missing required field 'title'. Required: title (string), items (list). Optional: description (string)"} |
| 440 482 | end |
| 441 483 | |
| 442 484 | defp validate_mega_menu_section(%{title: title}) when is_binary(title) do |
| 443 | - {:error, "Mega menu section missing required field 'items'. Required: title (string), items (list). Optional: description (string)"} |
| 485 | + {:error, |
| 486 | + "Mega menu section missing required field 'items'. Required: title (string), items (list). Optional: description (string)"} |
| 444 487 | end |
| 445 488 | |
| 446 489 | defp validate_mega_menu_section(%{title: title}) do |
| @@ -452,7 +495,8 @@ defmodule NavBuddy do | |
| 452 495 | end |
| 453 496 | |
| 454 497 | defp validate_mega_menu_section(section) do |
| 455 | - {:error, "Mega menu section must have 'title' (string) and 'items' (list). Got: #{inspect(section)}"} |
| 498 | + {:error, |
| 499 | + "Mega menu section must have 'title' (string) and 'items' (list). Got: #{inspect(section)}"} |
| 456 500 | end |
| 457 501 | |
| 458 502 | @doc """ |
| @@ -468,9 +512,9 @@ defmodule NavBuddy do | |
| 468 512 | """ |
| 469 513 | def nav_item(id, label, navigate \\ nil, opts \\ []) do |
| 470 514 | base = %{id: id, label: label} |
| 471 | - |
| 515 | + |
| 472 516 | base = if navigate, do: Map.put(base, :navigate, navigate), else: base |
| 473 | - |
| 517 | + |
| 474 518 | Enum.reduce(opts, base, fn {key, value}, acc -> |
| 475 519 | Map.put(acc, key, value) |
| 476 520 | end) |
| @@ -490,7 +534,7 @@ defmodule NavBuddy do | |
| 490 534 | """ |
| 491 535 | def dropdown_item(id, label, dropdown_items, opts \\ []) do |
| 492 536 | base = %{id: id, label: label, dropdown: dropdown_items} |
| 493 | - |
| 537 | + |
| 494 538 | Enum.reduce(opts, base, fn {key, value}, acc -> |
| 495 539 | Map.put(acc, key, value) |
| 496 540 | end) |
| @@ -510,7 +554,7 @@ defmodule NavBuddy do | |
| 510 554 | """ |
| 511 555 | def mega_menu_item(id, label, categories, opts \\ []) do |
| 512 556 | base = %{id: id, label: label, mega_menu: categories} |
| 513 | - |
| 557 | + |
| 514 558 | Enum.reduce(opts, base, fn {key, value}, acc -> |
| 515 559 | Map.put(acc, key, value) |
| 516 560 | end) |
| @@ -157,7 +157,7 @@ defmodule NavBuddy.Component do | |
| 157 157 | ~H""" |
| 158 158 | <div class={@item_classes} {@item_attributes}> |
| 159 159 | <%= cond do %> |
| 160 | - <% @item.mega_menu -> %> |
| 160 | + <% Map.get(@item, :mega_menu) -> %> |
| 161 161 | <.mega_menu_trigger item={@item} config={@config} nav_id={@nav_id} /> |
| 162 162 | <.mega_menu |
| 163 163 | menu={@item.mega_menu} |
| @@ -166,7 +166,7 @@ defmodule NavBuddy.Component do | |
| 166 166 | item_id={@item.id} |
| 167 167 | /> |
| 168 168 | |
| 169 | - <% @item.dropdown -> %> |
| 169 | + <% Map.get(@item, :dropdown) -> %> |
| 170 170 | <.dropdown_trigger item={@item} config={@config} nav_id={@nav_id} /> |
| 171 171 | <.dropdown_menu |
| 172 172 | items={@item.dropdown} |
| @@ -192,26 +192,26 @@ defmodule NavBuddy.Component do | |
| 192 192 | |
| 193 193 | def nav_link(assigns) do |
| 194 194 | ~H""" |
| 195 | - <%= if @item.navigate do %> |
| 195 | + <%= if Map.get(@item, :navigate) do %> |
| 196 196 | <a |
| 197 197 | href="#" |
| 198 | - class={["navbuddy-nav-link", if(@item.active, do: "active"), if(@item.disabled, do: "disabled")]} |
| 198 | + class={["navbuddy-nav-link", if(Map.get(@item, :active), do: "active"), if(Map.get(@item, :disabled), do: "disabled")]} |
| 199 199 | role="menuitem" |
| 200 | - tabindex={if @item.disabled, do: "-1", else: "0"} |
| 201 | - aria-disabled={to_string(!!@item.disabled)} |
| 202 | - phx-click={JS.push("navbuddy:item_click", value: %{id: @item.id, navigate: @item.navigate})} |
| 200 | + tabindex={if Map.get(@item, :disabled), do: "-1", else: "0"} |
| 201 | + aria-disabled={to_string(!!Map.get(@item, :disabled))} |
| 202 | + phx-click={JS.push("navbuddy:item_click", value: %{id: @item.id, navigate: Map.get(@item, :navigate)})} |
| 203 203 | > |
| 204 | - <.nav_icon :if={@item.icon} icon={@item.icon} /> |
| 204 | + <.nav_icon :if={Map.get(@item, :icon)} icon={Map.get(@item, :icon)} /> |
| 205 205 | <span class="navbuddy-nav-text">{@item.label}</span> |
| 206 206 | </a> |
| 207 207 | <% else %> |
| 208 208 | <span |
| 209 | - class={["navbuddy-nav-link", "no-navigate", if(@item.disabled, do: "disabled")]} |
| 209 | + class={["navbuddy-nav-link", "no-navigate", if(Map.get(@item, :disabled), do: "disabled")]} |
| 210 210 | role="menuitem" |
| 211 | - tabindex={if @item.disabled, do: "-1", else: "0"} |
| 212 | - aria-disabled={to_string(!!@item.disabled)} |
| 211 | + tabindex={if Map.get(@item, :disabled), do: "-1", else: "0"} |
| 212 | + aria-disabled={to_string(!!Map.get(@item, :disabled))} |
| 213 213 | > |
| 214 | - <.nav_icon :if={@item.icon} icon={@item.icon} /> |
| 214 | + <.nav_icon :if={Map.get(@item, :icon)} icon={Map.get(@item, :icon)} /> |
| 215 215 | <span class="navbuddy-nav-text">{@item.label}</span> |
| 216 216 | </span> |
| 217 217 | <% end %> |
| @@ -328,6 +328,27 @@ defmodule NavBuddy.Component do | |
| 328 328 | |
| 329 329 | @doc """ |
| 330 330 | Renders a mega menu. |
| 331 | + |
| 332 | + Supports two formats: |
| 333 | + |
| 334 | + 1. Simple format: %{"Category Name" => [nav_items]} |
| 335 | + 2. Structured format: %{title: "Title", description: "Description", sections: [%{title: "Section", items: [nav_items]}]} |
| 336 | + |
| 337 | + ## Examples |
| 338 | + |
| 339 | + # Simple mega menu |
| 340 | + <.mega_menu |
| 341 | + menu={%{"Frontend" => [nav_items], "Backend" => [nav_items]}} |
| 342 | + config={@config} |
| 343 | + nav_id="main-nav" |
| 344 | + item_id="tools" /> |
| 345 | + |
| 346 | + # Structured mega menu |
| 347 | + <.mega_menu |
| 348 | + menu={%{title: "Our Tools", sections: [%{title: "Frontend", items: [nav_items]}]}} |
| 349 | + config={@config} |
| 350 | + nav_id="main-nav" |
| 351 | + item_id="tools" /> |
| 331 352 | """ |
| 332 353 | attr(:menu, :map, required: true) |
| 333 354 | attr(:config, NavBuddy.Config, required: true) |
| @@ -346,42 +367,70 @@ defmodule NavBuddy.Component do | |
| 346 367 | aria-labelledby={"#{@nav_id}-#{@item_id}-trigger"} |
| 347 368 | > |
| 348 369 | <div class="navbuddy-mega-menu-container"> |
| 349 | - <div class="navbuddy-mega-menu-header"> |
| 350 | - <h3 class="navbuddy-mega-menu-title">{@menu.title}</h3> |
| 351 | - <p :if={@menu.description} class="navbuddy-mega-menu-description"> |
| 352 | - {@menu.description} |
| 353 | - </p> |
| 354 | - </div> |
| 355 | - |
| 356 | - <div class="navbuddy-mega-menu-grid"> |
| 357 | - <div |
| 358 | - :for={section <- @menu.sections} |
| 359 | - class="navbuddy-mega-menu-section" |
| 360 | - role="group" |
| 361 | - aria-labelledby={"#{@menu_id}-section-#{section.title |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 362 | - > |
| 363 | - <h4 |
| 364 | - id={"#{@menu_id}-section-#{section.title |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 365 | - class="navbuddy-mega-menu-section-title" |
| 366 | - > |
| 367 | - {section.title} |
| 368 | - </h4> |
| 369 | - <p :if={section.description} class="navbuddy-mega-menu-section-description"> |
| 370 | - {section.description} |
| 370 | + <%= if is_structured_mega_menu?(@menu) do %> |
| 371 | + <div class="navbuddy-mega-menu-header"> |
| 372 | + <h3 class="navbuddy-mega-menu-title">{@menu.title}</h3> |
| 373 | + <p :if={Map.get(@menu, :description)} class="navbuddy-mega-menu-description"> |
| 374 | + {Map.get(@menu, :description)} |
| 371 375 | </p> |
| 372 | - |
| 373 | - <ul class="navbuddy-mega-menu-section-items"> |
| 374 | - <li :for={item <- section.items} class="navbuddy-mega-menu-item" role="none"> |
| 375 | - <.nav_link item={item} config={@config} /> |
| 376 | - </li> |
| 377 | - </ul> |
| 378 376 | </div> |
| 379 | - </div> |
| 377 | + |
| 378 | + <div class="navbuddy-mega-menu-grid"> |
| 379 | + <div |
| 380 | + :for={section <- @menu.sections} |
| 381 | + class="navbuddy-mega-menu-section" |
| 382 | + role="group" |
| 383 | + aria-labelledby={"#{@menu_id}-section-#{section.title |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 384 | + > |
| 385 | + <h4 |
| 386 | + id={"#{@menu_id}-section-#{section.title |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 387 | + class="navbuddy-mega-menu-section-title" |
| 388 | + > |
| 389 | + {section.title} |
| 390 | + </h4> |
| 391 | + <p :if={Map.get(section, :description)} class="navbuddy-mega-menu-section-description"> |
| 392 | + {Map.get(section, :description)} |
| 393 | + </p> |
| 394 | + |
| 395 | + <ul class="navbuddy-mega-menu-section-items"> |
| 396 | + <li :for={item <- section.items} class="navbuddy-mega-menu-item" role="none"> |
| 397 | + <.nav_link item={item} config={@config} /> |
| 398 | + </li> |
| 399 | + </ul> |
| 400 | + </div> |
| 401 | + </div> |
| 402 | + <% else %> |
| 403 | + <div class="navbuddy-mega-menu-grid"> |
| 404 | + <div |
| 405 | + :for={{category_name, items} <- @menu} |
| 406 | + class="navbuddy-mega-menu-section" |
| 407 | + role="group" |
| 408 | + aria-labelledby={"#{@menu_id}-category-#{category_name |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 409 | + > |
| 410 | + <h4 |
| 411 | + id={"#{@menu_id}-category-#{category_name |> String.downcase() |> String.replace(~r/[^a-z0-9]/, "-")}"} |
| 412 | + class="navbuddy-mega-menu-section-title" |
| 413 | + > |
| 414 | + {category_name} |
| 415 | + </h4> |
| 416 | + |
| 417 | + <ul class="navbuddy-mega-menu-section-items"> |
| 418 | + <li :for={item <- items} class="navbuddy-mega-menu-item" role="none"> |
| 419 | + <.nav_link item={item} config={@config} /> |
| 420 | + </li> |
| 421 | + </ul> |
| 422 | + </div> |
| 423 | + </div> |
| 424 | + <% end %> |
| 380 425 | </div> |
| 381 426 | </div> |
| 382 427 | """ |
| 383 428 | end |
| 384 429 | |
| 430 | + # Helper function to determine mega menu type |
| 431 | + defp is_structured_mega_menu?(%{title: _, sections: _}), do: true |
| 432 | + defp is_structured_mega_menu?(_), do: false |
| 433 | + |
| 385 434 | @doc """ |
| 386 435 | Renders the mobile navigation toggle button. |
| 387 436 | """ |
| @@ -588,10 +637,10 @@ defmodule NavBuddy.Component do | |
| 588 637 | defp assign_item_data(assigns) do |
| 589 638 | item_classes = [ |
| 590 639 | "navbuddy-nav-item-wrapper", |
| 591 | - if(assigns.item.active, do: "active"), |
| 592 | - if(assigns.item.disabled, do: "disabled"), |
| 593 | - if(assigns.item.dropdown, do: "has-dropdown"), |
| 594 | - if(assigns.item.mega_menu, do: "has-mega-menu") |
| 640 | + if(Map.get(assigns.item, :active), do: "active"), |
| 641 | + if(Map.get(assigns.item, :disabled), do: "disabled"), |
| 642 | + if(Map.get(assigns.item, :dropdown), do: "has-dropdown"), |
| 643 | + if(Map.get(assigns.item, :mega_menu), do: "has-mega-menu") |
| 595 644 | ] |
| 596 645 | |
| 597 646 | item_attributes = %{ |
| @@ -1,7 +1,7 @@ | |
| 1 1 | defmodule NavBuddy.MixProject do |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 | - @version "0.2.0" |
| 4 | + @version "0.2.3" |
| 5 5 | @source_url "https://github.com/SangRJ/navbuddy" |
| 6 6 | |
| 7 7 | def project do |