axum-admin 0.1.1

A modern admin dashboard framework for Axum
Documentation
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{% block title %}{{ admin_title }}{% endblock %}</title>
  <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          fontFamily: { sans: ['Inter', 'sans-serif'] },
          colors: {
            "surface":                   "#fbf8fc",
            "on-surface":                "#1b1b1e",
            "on-surface-variant":        "#474747",
            "surface-container":         "#f0edf1",
            "surface-container-low":     "#f6f2f7",
            "surface-container-high":    "#eae7eb",
            "surface-container-highest": "#e4e1e6",
            "surface-container-lowest":  "#ffffff",
            "outline-variant":           "#c6c6c6",
            "primary":                   "#000000",
            "on-primary":                "#ffffff",
            "error":                     "#ba1a1a",
          },
        },
      },
    }
  </script>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
  <link rel="stylesheet" href="/admin/_static/admin.css" />
  <script>
    document.addEventListener('alpine:init', () => {
      Alpine.data('multiSelect', () => ({
        options: [],
        selected: [],
        open: false,
        init() {
          this.options = JSON.parse(this.$el.dataset.options || '[]');
          this.selected = JSON.parse(this.$el.dataset.selected || '[]');
        },
        toggle(val) {
          const i = this.selected.indexOf(val);
          if (i === -1) this.selected.push(val);
          else this.selected.splice(i, 1);
        },
        label(val) {
          const opt = this.options.find(o => o[0] === val);
          return opt ? opt[1] : val;
        }
      }));
    });
  </script>
  <script defer src="/admin/_static/alpine.min.js"></script>
  <script src="/admin/_static/htmx.min.js"></script>
</head>
<body class="bg-surface text-on-surface antialiased flex h-screen overflow-hidden font-sans">

  <!-- Sidebar -->
  <aside class="w-64 shrink-0 bg-zinc-50 border-r border-zinc-200 flex flex-col py-6 px-4 h-screen overflow-y-auto">
    <a href="/admin/" class="flex items-center gap-3 px-2 mb-8 group">
      <div class="w-8 h-8 bg-black rounded-md flex items-center justify-center group-hover:bg-zinc-700 transition-colors">
        <i class="{{ admin_icon }} text-white text-xs"></i>
      </div>
      <span class="text-sm font-bold tracking-tight text-zinc-900 group-hover:text-zinc-600 transition-colors">{{ admin_title }}</span>
    </a>

    <nav class="flex-1 space-y-1">
      {% for item in nav %}
        {% if item.type == "entity" %}
        <a href="/admin/{{ item.name }}/"
           class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors
                  {% if current_entity == item.name %}bg-zinc-200/60 text-zinc-900 font-semibold{% else %}text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100{% endif %}">
          <i class="{{ item.icon }} text-xs w-4"></i>
          {{ item.label }}
        </a>
        {% elif item.type == "group" %}
        <div x-data="{ open: {{ 'true' if item.active else 'false' }} }" class="space-y-0.5">
          <button @click="open = !open"
                  class="w-full flex items-center justify-between px-3 py-2 rounded-md text-xs font-semibold uppercase tracking-wider text-zinc-400 hover:text-zinc-700 hover:bg-zinc-100 transition-colors">
            <span>{{ item.label }}</span>
            <i class="fa-solid fa-chevron-down text-[10px] transition-transform duration-200" :class="open ? 'rotate-180' : ''"></i>
          </button>
          <div x-show="open" class="space-y-0.5">
            {% for child in item.entities %}
            <a href="/admin/{{ child.name }}/"
               class="flex items-center gap-3 pl-6 pr-3 py-2 rounded-md text-sm font-medium transition-colors
                      {% if current_entity == child.name %}bg-zinc-200/60 text-zinc-900 font-semibold{% else %}text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100{% endif %}">
              <i class="{{ child.icon }} text-xs w-4"></i>
              {{ child.label }}
            </a>
            {% endfor %}
          </div>
        </div>
        {% endif %}
      {% endfor %}
    </nav>

    {% if show_auth_nav %}
    <div class="pt-4 border-t border-zinc-200 space-y-1 mb-2">
      <p class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-zinc-400">Auth</p>
      <a href="/admin/users/"
         class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors
                {% if current_entity == '__users' %}bg-zinc-200/60 text-zinc-900 font-semibold{% else %}text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100{% endif %}">
        <i class="fa-solid fa-users text-xs w-4"></i>
        Users
      </a>
      <a href="/admin/roles/"
         class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors
                {% if current_entity == '__roles' %}bg-zinc-200/60 text-zinc-900 font-semibold{% else %}text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100{% endif %}">
        <i class="fa-solid fa-shield-halved text-xs w-4"></i>
        Roles
      </a>
    </div>
    {% endif %}

    <div class="pt-4 border-t border-zinc-200 space-y-1">
      <a href="/admin/change-password"
         class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 transition-colors">
        <i class="fa-solid fa-key text-xs w-4"></i>
        Change password
      </a>
      <a href="/admin/logout"
         class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 transition-colors">
        <i class="fa-solid fa-right-from-bracket text-xs w-4"></i>
        Logout
      </a>
    </div>
  </aside>

  <!-- Main area -->
  <div class="flex-1 flex flex-col min-w-0 overflow-hidden">

    <!-- Header -->
    <header class="sticky top-0 z-40 h-14 border-b border-zinc-200 bg-white/80 backdrop-blur-md flex items-center justify-between px-6 shrink-0">
      <div class="flex-1"></div>
      <div class="flex items-center gap-3">
        <div class="w-7 h-7 rounded-full bg-zinc-900 flex items-center justify-center text-white text-xs font-bold">A</div>
      </div>
    </header>

    <!-- Flash messages -->
    <div id="flash" class="px-8">
      {% if flash_success %}
      <div class="mt-4 px-4 py-3 rounded-md bg-emerald-50 border border-emerald-200 text-emerald-800 text-sm font-medium flex items-center gap-2">
        <i class="fa-solid fa-circle-check text-emerald-500"></i>
        {{ flash_success }}
      </div>
      {% endif %}
      {% if flash_error %}
      <div class="mt-4 px-4 py-3 rounded-md bg-red-50 border border-red-200 text-red-700 text-sm font-medium flex items-center gap-2">
        <i class="fa-solid fa-circle-exclamation text-red-500"></i>
        {{ flash_error }}
      </div>
      {% endif %}
    </div>

    <!-- Page content -->
    <main class="flex-1 overflow-y-auto p-8">
      {% block content %}{% endblock %}
    </main>
  </div>
</body>
</html>