The button element now takes an `icon`, so a page gets one by passing icon="name" rather than by hand-rolling its own button markup. Every button on the account and MFA screens carries one; a test walks each page and asserts no button is left bare. Change password: labels dropped (allauth already sets a placeholder on each field), the current password set apart from the new pair, help text kept on the new password, and Forgot Password promoted from a bare link to an accent button. MFA management: recovery-code actions are now ranked -- View is primary, Download and Generate are outline. Generate silently invalidates the codes you already hold, so it must not read as the obvious thing to click. Panel actions get breathing room from the body text (card-actions mt-4). Viewing recovery codes: Download and Generate sit side by side instead of stacking. TOTP activate: the code box loses its heading -- an otp field never takes a visible label, the boxes say what they are -- and the authenticator secret gets margin around it, since it is copied out by hand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
7.3 KiB
HTML
156 lines
7.3 KiB
HTML
{% load lucide static ui %}
|
|
|
|
{% comment %}
|
|
The page skeleton, with no branding of its own. `_platform_base.html` dresses it
|
|
as RosterChief, `_club_base.html` as a club; the `branding` context processor
|
|
picks between them per tenant.
|
|
{% endcomment %}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<title>
|
|
{% block title %}RosterChief{% endblock title %}
|
|
</title>
|
|
|
|
{% comment %}
|
|
Apply the stored theme before first paint, otherwise the page flashes the wrong
|
|
colours. Nothing stored means "auto": we set no attribute at all, so daisyUI's
|
|
`dark --prefersdark` follows the OS.
|
|
{% endcomment %}
|
|
<script>
|
|
(() => {
|
|
const stored = localStorage.getItem("theme");
|
|
if (stored) document.documentElement.setAttribute("data-theme", stored);
|
|
})();
|
|
</script>
|
|
|
|
<link rel="stylesheet" href="{% static 'css/app.css' %}"/>
|
|
|
|
{# After the stylesheet: brand overrides (logo urls, club colours) must win. #}
|
|
{% block extra %}{% endblock extra %}
|
|
</head>
|
|
|
|
<body class="min-h-screen bg-base-200">
|
|
<div class="navbar mb-4 border-b border-base-300 bg-base-100 px-6 shadow-sm">
|
|
<div class="my-4 flex-1">
|
|
{% block brand %}{% endblock brand %}
|
|
</div>
|
|
|
|
<button class="btn btn-ghost w-24" type="button" data-theme-toggle aria-label="Theme">
|
|
<span data-theme-icon="light" class="hidden items-center gap-4">{% lucide "sun" size=20 %} light</span>
|
|
<span data-theme-icon="dark" class="hidden items-center gap-4">{% lucide "moon" size=20 %} dark</span>
|
|
<span data-theme-icon="auto" class="hidden items-center gap-4">{% lucide "sun-moon" size=20 %} auto</span>
|
|
</button>
|
|
|
|
{% if user.is_authenticated %}
|
|
<div class="dropdown dropdown-end">
|
|
<div tabindex="0" role="button" class="btn btn-ghost gap-4">{% lucide "circle-user" %}{{ user.get_full_name }}</div>
|
|
<ul tabindex="0" class="menu dropdown-content z-10 mt-2 w-60 rounded-box bg-base-100 p-2 shadow border border-base-content/20">
|
|
<li>
|
|
<a href="{% url 'mfa_index' %}">{% lucide "shield-check" size=16 %} Two-factor authentication</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% url 'account_change_password' %}">{% lucide "key-round" size=16 %} Change password</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% url 'account_logout' %}">{% lucide "log-out" size=16 %} Sign out</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{% else %}
|
|
<a class="btn btn-ghost gap-4" href="{% url 'account_login' %}">{% lucide "log-in" size=16 %} Sign in</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if messages %}
|
|
<div class="mx-auto mt-4 w-full space-y-2 px-4">
|
|
{% for message in messages %}
|
|
{% with alert=message|as_alert %}
|
|
<div class="alert alert-soft {{ alert.css }}" role="alert">
|
|
{% lucide alert.icon size=20 %}
|
|
<div>
|
|
<div class="font-bold">{{ alert.title }}</div>
|
|
<div class="text-sm">{{ alert.body }}</div>
|
|
</div>
|
|
</div>
|
|
{% endwith %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<main class="mx-auto w-full p-4">
|
|
{% block main %}{% endblock main %}
|
|
</main>
|
|
|
|
<script>
|
|
// The button cycles light -> dark -> auto. "auto" removes the attribute and the
|
|
// stored key rather than writing the OS's current choice: that keeps daisyUI's
|
|
// `dark --prefersdark` following the OS *live*, so the page flips when the OS
|
|
// does. Storing a snapshot would freeze it at whatever the OS was on click.
|
|
const MODES = ["light", "dark", "auto"];
|
|
const currentMode = () => localStorage.getItem("theme") || "auto";
|
|
|
|
const applyMode = (mode) => {
|
|
if (mode === "auto") {
|
|
localStorage.removeItem("theme");
|
|
document.documentElement.removeAttribute("data-theme");
|
|
} else {
|
|
localStorage.setItem("theme", mode);
|
|
document.documentElement.setAttribute("data-theme", mode);
|
|
}
|
|
|
|
// Show the label for the *chosen* mode, not the resulting colours -- otherwise
|
|
// "auto" would be indistinguishable from whichever theme it resolved to.
|
|
// `hidden` and `inline-flex` are both display utilities, so the visible one
|
|
// must carry exactly one of them: leaving both on would let stylesheet order,
|
|
// not class order, decide who wins.
|
|
document.querySelectorAll("[data-theme-icon]").forEach((label) => {
|
|
const active = label.dataset.themeIcon === mode;
|
|
label.classList.toggle("hidden", !active);
|
|
label.classList.toggle("inline-flex", active);
|
|
});
|
|
document.querySelectorAll("[data-theme-toggle]").forEach((button) => button.setAttribute("aria-label", `Theme: ${mode}`));
|
|
};
|
|
|
|
document.querySelectorAll("[data-theme-toggle]").forEach((button) => {
|
|
button.addEventListener("click", () => applyMode(MODES[(MODES.indexOf(currentMode()) + 1) % MODES.length]));
|
|
});
|
|
|
|
applyMode(currentMode());
|
|
</script>
|
|
|
|
<script>
|
|
// A TOTP code is 6 characters, a recovery code 8, and allauth accepts either in
|
|
// the same field. The boxed otp layout only fits six, so past that we fall back
|
|
// to a plain input rather than letting the text spill out of the boxes. Boxing
|
|
// the field to six and calling it done would lock recovery codes out entirely.
|
|
document.querySelectorAll("[data-otp]").forEach((otp) => {
|
|
const input = otp.querySelector("input");
|
|
if (!input) return;
|
|
|
|
const fit = () => {
|
|
const boxed = input.value.length <= 6;
|
|
otp.classList.toggle("otp", boxed);
|
|
otp.classList.toggle("otp-lg", boxed);
|
|
otp.querySelectorAll("span").forEach((box) => box.classList.toggle("hidden", !boxed));
|
|
input.classList.toggle("input", !boxed);
|
|
input.classList.toggle("input-lg", !boxed);
|
|
};
|
|
|
|
input.addEventListener("input", fit);
|
|
fit();
|
|
});
|
|
</script>
|
|
|
|
{% comment %}
|
|
allauth puts page-level scripts and out-of-form markup here — notably the
|
|
hidden `mfa_login` form the passkey button submits. Without this block that
|
|
form is never rendered and "Sign in with a passkey" is a dead button.
|
|
{% endcomment %}
|
|
{% block extra_body %}{% endblock extra_body %}
|
|
</body>
|
|
</html>
|