{% extends "admin/wrapper.html" %} {% block extra_head %}
<style>
body {
font-family: "Inter", sans-serif;
-webkit-font-smoothing: antialiased;
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}
#umbral-admin-sidebar,
#sidebar-nav {
scrollbar-width: thin;
scrollbar-color: var(--outline-variant) var(--surface-container);
}
#umbral-admin-sidebar::-webkit-scrollbar { width: 4px; }
#sidebar-nav::-webkit-scrollbar { width: 3px; }
#umbral-admin-sidebar::-webkit-scrollbar-track,
#sidebar-nav::-webkit-scrollbar-track {
background: var(--surface-container);
}
#umbral-admin-sidebar::-webkit-scrollbar-thumb,
#sidebar-nav::-webkit-scrollbar-thumb {
background: var(--outline-variant);
border-radius: 10px;
}
html {
scrollbar-width: thin;
scrollbar-color: var(--outline-variant) var(--surface-container);
}
.overflow-auto,
.overflow-y-auto,
.overflow-x-auto,
.overflow-y-scroll,
.overflow-x-scroll {
scrollbar-width: thin;
scrollbar-color: var(--outline-variant) var(--surface-container);
}
html ::-webkit-scrollbar,
.overflow-auto::-webkit-scrollbar,
.overflow-y-auto::-webkit-scrollbar,
.overflow-x-auto::-webkit-scrollbar,
.overflow-y-scroll::-webkit-scrollbar,
.overflow-x-scroll::-webkit-scrollbar {
width: 4px;
height: 4px;
}
html ::-webkit-scrollbar-track,
.overflow-auto::-webkit-scrollbar-track,
.overflow-y-auto::-webkit-scrollbar-track,
.overflow-x-auto::-webkit-scrollbar-track,
.overflow-y-scroll::-webkit-scrollbar-track,
.overflow-x-scroll::-webkit-scrollbar-track {
background: var(--surface-container);
}
html ::-webkit-scrollbar-thumb,
.overflow-auto::-webkit-scrollbar-thumb,
.overflow-y-auto::-webkit-scrollbar-thumb,
.overflow-x-auto::-webkit-scrollbar-thumb,
.overflow-y-scroll::-webkit-scrollbar-thumb,
.overflow-x-scroll::-webkit-scrollbar-thumb {
background: var(--outline-variant);
border-radius: 10px;
}
html ::-webkit-scrollbar-thumb:hover,
.overflow-auto::-webkit-scrollbar-thumb:hover,
.overflow-y-auto::-webkit-scrollbar-thumb:hover,
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: var(--outline);
}
html ::-webkit-scrollbar-corner {
background: transparent;
}
.topbar-glass {
background: color-mix(
in srgb,
var(--surface-container) 85%,
transparent
);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.sidebar-model-link {
font-size: 13px;
line-height: 18px;
}
</style>
{% endblock %} {% block body_attrs %}class="bg-background text-on-background
antialiased flex admin-shell" id="umbral-admin-body"{% endblock %} {% block body %}
<button
id="umbral-admin-mobile-scrim"
type="button"
class="fixed inset-0 z-40 bg-background/65 backdrop-blur-sm"
onclick="umbral.closeSidebar()"
aria-label="Close navigation"
></button>
<aside
id="umbral-admin-sidebar"
class="fixed left-0 top-0 h-full bg-surface-container border-r border-outline-variant flex flex-col z-50 overflow-y-auto"
aria-label="Admin navigation"
>
<div class="p-md flex items-center gap-sm flex-shrink-0">
<div
class="w-9 h-9 rounded-lg bg-primary text-on-primary flex items-center justify-center flex-shrink-0"
>
<i
data-lucide="layers"
class="w-[18px] h-[18px]"
></i>
</div>
<div class="sidebar-text min-w-0 flex-1">
<h2 class="font-h3 text-h3 text-on-surface leading-none">
{{ site_title }}
</h2>
<p
class="sidebar-meta text-[10px] text-outline tracking-widest uppercase mt-0.5 truncate"
>
{% if site_description %}{{ site_description }}{% elif admin_version %}{{ admin_version }}{% endif %}
</p>
</div>
<button
type="button"
class="md:hidden w-8 h-8 flex items-center justify-center rounded-lg text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all"
onclick="umbral.closeSidebar()"
aria-label="Close navigation"
>
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="sidebar-search-wrap px-md mb-md flex-shrink-0">
<div class="relative group">
<i
data-lucide="search"
class="absolute left-3 top-1/2 -translate-y-1/2 w-[16px] h-[16px] text-outline pointer-events-none"
></i>
<input
id="sidebar-search"
type="text"
placeholder="Search models"
oninput="umbral.sidebarFilter(this.value)"
class="w-full bg-surface-container-high border border-transparent rounded-xl pl-9 pr-md py-2 text-body-sm text-on-surface focus:outline-none focus:ring-1 focus:ring-outline focus:border-transparent transition-all placeholder:text-outline"
aria-label="Filter sidebar models"
/>
</div>
</div>
<nav
class="flex-1 overflow-y-auto px-sm space-y-md pb-md"
id="sidebar-nav"
aria-label="Models"
>
<div id="sidebar-group-core">
<p
class="sidebar-section-label px-md mb-xs font-label-sm text-label-sm text-outline uppercase tracking-wider"
>
Core
</p>
<div class="space-y-1 px-md">
<a
href="{{ admin_base }}/{% if restore_last_path %}?dashboard=1{% endif %}"
class="sidebar-link sidebar-model-link flex items-center gap-2 px-sm py-1.5 rounded-xl border border-transparent {% if active_table == '' %}bg-primary-container text-on-primary-container{% else %}text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface{% endif %} font-body-sm transition-all"
data-model-name="dashboard"
data-sidebar-tooltip="Dashboard"
aria-label="Dashboard"
>
<i
data-lucide="layout-dashboard"
class="w-[18px] h-[18px] flex-shrink-0"
></i>
<span class="sidebar-text truncate">Dashboard</span>
</a>
</div>
</div>
{%- for app in apps %}
<div class="sidebar-plugin-group" id="sidebar-group-{{ app.plugin }}">
<p
class="sidebar-section-label px-md mb-xs font-label-sm text-label-sm text-outline uppercase tracking-wider"
>
{{ app.label }}
</p>
<div class="space-y-1 px-md">
{%- for reg in app.models %}
<a
href="{{ admin_base }}/{{ reg.table }}/"
class="sidebar-link sidebar-model-link flex items-center gap-2 px-sm py-1.5 rounded-xl border border-transparent {% if active_table == reg.table %}bg-primary-container text-on-primary-container{% else %}text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface{% endif %} font-body-sm transition-all"
data-model-name="{{ reg.label | lower }}"
data-sidebar-tooltip="{{ reg.label }}"
aria-label="{{ reg.label }}"
>
<i
data-lucide="{{ reg.icon }}"
class="w-[18px] h-[18px] flex-shrink-0"
></i>
<span class="sidebar-text flex-1 truncate">{{ reg.label }}</span>
</a>
{%- endfor %}
</div>
</div>
{%- endfor %}
{%- for vg in view_groups | default(value=[]) %}
<div class="sidebar-plugin-group" id="sidebar-group-view-{{ loop.index }}">
<p class="sidebar-section-label px-md mb-xs font-label-sm text-label-sm text-outline uppercase tracking-wider">{{ vg.label }}</p>
<div class="space-y-1 px-md">
{%- for v in vg.views %}
<a
href="{{ v.href | safe }}"
class="sidebar-link sidebar-model-link flex items-center gap-2 px-sm py-1.5 rounded-xl border border-transparent {% if active_view is defined and active_view == v.slug %}bg-primary-container text-on-primary-container{% else %}text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface{% endif %} font-body-sm transition-all"
data-sidebar-tooltip="{{ v.label }}"
aria-label="{{ v.label }}"
>
<i data-lucide="{{ v.icon }}" class="w-[18px] h-[18px] flex-shrink-0"></i>
<span class="sidebar-text flex-1 truncate">{{ v.label }}</span>
</a>
{%- endfor %}
</div>
</div>
{%- endfor %}
</nav>
<div
class="p-md border-t border-outline-variant bg-surface-container-low flex-shrink-0"
>
<a
href="{{ admin_base }}/logout"
class="sidebar-link flex items-center gap-3 px-md py-2 rounded-xl text-on-surface-variant hover:text-on-surface hover:bg-surface-container-high font-body-sm transition-all"
data-sidebar-tooltip="Sign out"
aria-label="Sign out"
>
<i data-lucide="log-out" class="w-[18px] h-[18px]"></i>
<span class="sidebar-footer-text">Sign out</span>
</a>
</div>
</aside>
<div id="umbral-admin-main" class="flex-1 flex flex-col min-h-screen min-w-0">
<header
id="umbral-admin-topbar"
class="sticky top-0 z-30 h-topbar-height topbar-glass border-b border-outline-variant flex items-center justify-between gap-md px-md sm:px-lg"
aria-label="Admin top bar"
>
<div class="flex items-center gap-sm min-w-0">
<button
id="sidebar-toggle"
aria-label="Toggle navigation"
aria-expanded="true"
type="button"
onclick="umbral.toggleSidebar()"
class="w-9 h-9 flex items-center justify-center rounded-lg text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all flex-shrink-0"
>
<i data-lucide="panel-left" class="w-[18px] h-[18px]"></i>
</button>
<nav
class="flex items-center gap-xs font-label-md text-label-md min-w-0"
aria-label="Breadcrumb"
>
<a
href="{{ admin_base }}/{% if restore_last_path %}?dashboard=1{% endif %}"
class="text-outline hover:text-on-surface transition-colors shrink-0"
>Home</a
>
{%- for crumb in breadcrumbs %}
<i
data-lucide="chevron-right"
class="w-[14px] h-[14px] text-outline"
></i>
{%- if loop.last %}
<span class="text-primary font-semibold">{{ crumb.label }}</span>
{%- else %}
<a
href="{{ crumb.url }}"
class="text-outline hover:text-on-surface transition-colors"
>{{ crumb.label }}</a
>
{%- endif %} {%- endfor %}
</nav>
</div>
<button
id="cmd-palette-trigger"
aria-label="Open command palette (Ctrl+K)"
class="hidden md:flex bg-surface-container-high border border-transparent px-md py-1.5 rounded-xl items-center gap-xl hover:bg-surface-container-highest transition-colors min-w-[260px]"
onclick="
htmx.ajax('GET', '{{ admin_base }}/api/palette', {
target: '#umbral-palette-slot',
swap: 'innerHTML',
})
"
type="button"
>
<div class="flex items-center gap-sm">
<i
data-lucide="search"
class="w-[16px] h-[16px] text-outline"
></i>
<span class="text-body-sm text-outline"
>Search everything</span
>
</div>
<span
class="ml-sm inline-flex items-center gap-1 text-[10px] text-outline"
aria-hidden="true"
>
<kbd
class="rounded-md border border-transparent bg-surface-container-low px-1.5 py-0.5 font-medium text-on-surface-variant"
>Ctrl</kbd
>
<kbd
class="rounded-md border border-transparent bg-surface-container-low px-1.5 py-0.5 font-medium text-on-surface-variant"
>K</kbd
>
</span>
</button>
<div class="flex items-center gap-sm sm:gap-md">
<button
id="theme-toggle"
aria-label="Toggle light/dark theme"
type="button"
onclick="umbral.toggleTheme()"
class="w-9 h-9 flex items-center justify-center rounded-lg text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all"
>
<i
data-lucide="sun"
id="theme-icon-light"
class="w-[18px] h-[18px] hidden"
></i>
<i
data-lucide="moon"
id="theme-icon-dark"
class="w-[18px] h-[18px]"
></i>
</button>
<div class="hidden sm:block w-px h-6 bg-outline-variant"></div>
<div
class="hidden sm:flex items-center gap-3 cursor-pointer group"
role="button"
tabindex="0"
aria-label="User menu"
onclick="umbral.toggleUserMenu()"
>
<div class="text-right">
<p
class="text-body-sm font-semibold text-on-surface leading-none"
>
{{ user }}
</p>
<p
class="text-[10px] text-outline uppercase tracking-wider mt-0.5"
>
Staff
</p>
</div>
<div
class="w-9 h-9 rounded-lg bg-primary-container flex items-center justify-center border border-outline-variant group-hover:border-primary transition-colors"
>
<span
class="font-label-sm text-label-sm text-on-primary-container uppercase"
>{{ user[:2] }}</span
>
</div>
</div>
</div>
</header>
<main id="umbral-admin-content" class="flex-1 p-md sm:p-lg lg:p-xl overflow-x-hidden">
{% block content %}{% endblock %}
</main>
</div>
<div id="umbral-sidebar-tooltip" role="tooltip" aria-hidden="true"></div>
{% endblock %}