adminx-core 0.3.0

Framework-neutral core for the adminx admin-panel framework: the Resource trait, storage abstraction, registry, and neutral request/response types shared by every web-framework and database adapter.
Documentation
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>{{ title | default(value="adminx") }}</title>

  <script>window.tailwind = { config: { darkMode: 'class' } };</script>
  <script src="https://cdn.tailwindcss.com"></script>

  <!-- Prevent theme flash -->
  <script>
    (function () {
      try {
        var stored = localStorage.getItem('adminx-theme');
        var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        var theme = stored || (prefersDark ? 'dark' : 'light');
        if (theme === 'dark') document.documentElement.classList.add('dark');
      } catch (_) {}
    })();
  </script>

  <style>
    :root { --bg: #f1f5f9; --fg: #0f172a; }
    html.dark { --bg: #0b1220; --fg: #e5e7eb; }
    body { background: var(--bg) !important; color: var(--fg) !important; }
  </style>
</head>

<body class="h-full flex flex-col bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 selection:bg-indigo-200/50 dark:selection:bg-indigo-400/30">

  {% include "header.html" %}

  <main class="flex-1">
    <div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
      {% if toast %}
        <div class="mb-4 px-4 py-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 text-sm">{{ toast }}</div>
      {% endif %}
      {% block content %}{% endblock content %}
    </div>
  </main>

  {% include "footer.html" %}

  <script>
    (function () {
      // ===== THEME =====
      var root = document.documentElement;
      function applyTheme(theme) {
        if (theme === 'dark') root.classList.add('dark'); else root.classList.remove('dark');
        try { localStorage.setItem('adminx-theme', theme); } catch (_) {}
        document.querySelectorAll('[data-icon-sun]').forEach(function (el) { el.classList.toggle('hidden', theme !== 'dark'); });
        document.querySelectorAll('[data-icon-moon]').forEach(function (el) { el.classList.toggle('hidden', theme === 'dark'); });
      }
      applyTheme(root.classList.contains('dark') ? 'dark' : 'light');
      document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
        btn.addEventListener('click', function () { applyTheme(root.classList.contains('dark') ? 'light' : 'dark'); });
      });

      // ===== DESKTOP DROPDOWN MENUS =====
      var openId = null;
      function closeMenus() {
        document.querySelectorAll('[data-menu-panel]').forEach(function (p) {
          p.classList.add('pointer-events-none', 'opacity-0', 'translate-y-1', 'scale-95');
          p.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
        });
        document.querySelectorAll('[data-menu-trigger]').forEach(function (t) { t.setAttribute('aria-expanded', 'false'); });
        openId = null;
      }
      function openMenu(id) {
        closeMenus();
        var panel = document.querySelector('[data-menu-panel][data-id="' + id + '"]');
        var trigger = document.querySelector('[data-menu-trigger][data-id="' + id + '"]');
        if (!panel || !trigger) return;
        panel.classList.remove('pointer-events-none', 'opacity-0', 'translate-y-1', 'scale-95');
        panel.classList.add('opacity-100', 'translate-y-0', 'scale-100');
        trigger.setAttribute('aria-expanded', 'true');
        openId = id;
      }
      document.querySelectorAll('[data-menu-trigger]').forEach(function (btn) {
        btn.addEventListener('click', function (e) {
          e.stopPropagation();
          var id = btn.getAttribute('data-id');
          if (openId === id) { closeMenus(); } else { openMenu(id); }
        });
      });
      document.addEventListener('mousedown', function (e) {
        var nav = document.getElementById('desktop-nav');
        if (!nav || openId === null) return;
        if (!nav.contains(e.target)) closeMenus();
      });
      document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenus(); });
      document.querySelectorAll('[data-menu-item]').forEach(function (a) { a.addEventListener('click', closeMenus); });

      // ===== MOBILE DRAWER =====
      var drawer = document.getElementById('mobile-menu');
      var burger = document.querySelector('[data-mobile-toggle]');
      function setDrawer(open) {
        if (!drawer) return;
        drawer.classList.toggle('opacity-100', open);
        drawer.classList.toggle('opacity-0', !open);
        drawer.classList.toggle('pointer-events-auto', open);
        drawer.classList.toggle('pointer-events-none', !open);
      }
      if (burger) burger.addEventListener('click', function () { setDrawer(drawer.classList.contains('opacity-0')); });
      document.querySelectorAll('[data-mobile-link]').forEach(function (a) { a.addEventListener('click', function () { setDrawer(false); }); });
      document.querySelectorAll('[data-mobile-accordion]').forEach(function (btn) {
        btn.addEventListener('click', function () {
          var panel = btn.nextElementSibling;
          if (!panel) return;
          panel.classList.toggle('max-h-0');
          panel.classList.toggle('max-h-96');
          panel.classList.toggle('opacity-0');
          panel.classList.toggle('opacity-100');
          var icon = btn.querySelector('svg'); if (icon) icon.classList.toggle('rotate-180');
        });
      });
    })();
  </script>
</body>
</html>