adminx-core 3.0.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="Backup codes") }}</title>
  <script>window.tailwind = { config: { darkMode: 'class' } };</script>
  <script src="{{ tailwind_src | safe }}"></script>
  <script>
    (function () {
      try {
        var stored = localStorage.getItem('adminx-theme');
        var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        if ((stored || (prefersDark ? 'dark' : 'light')) === 'dark') document.documentElement.classList.add('dark');
      } catch (_) {}
    })();
  </script>
</head>
<body class="h-full 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">
  <div class="flex items-center justify-center min-h-screen px-4 py-10">
    <div class="bg-white dark:bg-gray-800 p-8 rounded-xl shadow-lg w-full max-w-md border border-gray-200 dark:border-gray-700">
      <div class="text-center mb-8">
        <div class="mx-auto w-16 h-16 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-full flex items-center justify-center mb-4">
          <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
          </svg>
        </div>
        <h2 class="text-2xl font-bold text-gray-900 dark:text-white">Two-factor auth enabled</h2>
        <p class="text-gray-600 dark:text-gray-400 mt-2">
          Save these one-time backup codes somewhere safe. Each works once if you lose your authenticator.
          <span class="font-semibold text-gray-800 dark:text-gray-200">They won't be shown again.</span>
        </p>
      </div>

      <div id="codes" class="grid grid-cols-2 gap-2 mb-4">
        {% for code in backup_codes %}
          <code class="backup-code text-center bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-2 text-sm text-gray-800 dark:text-gray-200 tracking-widest">{{ code }}</code>
        {% endfor %}
      </div>

      <div class="flex gap-3 mb-6">
        <button type="button" id="copy-btn"
          class="flex-1 flex justify-center items-center gap-2 py-2.5 px-4 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
          Copy
        </button>
        <button type="button" id="download-btn"
          class="flex-1 flex justify-center items-center gap-2 py-2.5 px-4 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
          Download
        </button>
      </div>

      <a href="{{ mount | safe }}"
        class="w-full flex justify-center items-center py-3 px-4 rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-indigo-600 to-fuchsia-600 hover:from-indigo-700 hover:to-fuchsia-700 transition-all duration-200">
        I've saved them — continue
      </a>
    </div>
  </div>

  <script>
    (function () {
      var codes = Array.prototype.map.call(
        document.querySelectorAll('.backup-code'),
        function (el) { return el.textContent.trim(); }
      );
      var text = codes.join('\n') + '\n';

      var copyBtn = document.getElementById('copy-btn');
      copyBtn.addEventListener('click', function () {
        var done = function () {
          var original = copyBtn.innerHTML;
          copyBtn.textContent = 'Copied!';
          setTimeout(function () { copyBtn.innerHTML = original; }, 1500);
        };
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(text).then(done, fallbackCopy);
        } else { fallbackCopy(); }
        function fallbackCopy() {
          var ta = document.createElement('textarea');
          ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
          document.body.appendChild(ta); ta.select();
          try { document.execCommand('copy'); done(); } catch (e) {}
          document.body.removeChild(ta);
        }
      });

      document.getElementById('download-btn').addEventListener('click', function () {
        var blob = new Blob([text], { type: 'text/plain' });
        var url = URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = url; a.download = 'adminx-backup-codes.txt';
        document.body.appendChild(a); a.click(); document.body.removeChild(a);
        URL.revokeObjectURL(url);
      });
    })();
  </script>
</body>
</html>