<!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">
<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>
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
<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>
<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>
<main class="flex-1 overflow-y-auto p-8">
{% block content %}{% endblock %}
</main>
</div>
</body>
</html>