Fix empty allauth forms, and lay out the 2FA page

The 2FA code input was not invisible -- it was absent, along with the fields of
every other allauth form except login.

Cause: the `fields` element passed `attrs.exclude` straight into a filter. On a
page that never sets it, resolving a filter *argument* raises
VariableDoesNotExist; Django rescues that for the main variable of an expression
but not for a filter argument, and {% if %} then swallows it and reads the
condition as false. So every field was skipped. Login was the one page that
passes `exclude`, which is exactly why it kept working and hid the damage.
`exclude` is now pinned to a real variable first, with tests that render the
login, signup and password-reset forms and assert their inputs exist.

Two dangling buttons fixed while in here: `elements/form.html` dropped the `id`
attribute, so the out-of-band forms allauth generates (webauthn_form,
logout-from-stage) had no id for a button's `form` attribute to point at. "Use a
security key" submitted nothing.

Layout: the code is a daisyUI otp field, Cancel sits beside Sign In as a plain
button, both gain icons, and "Use a security key" becomes an accent button.

The otp boxes yield once more than six characters are typed. allauth accepts a
TOTP code (6) *or* a recovery code (8) in this one field, so hard-boxing it to
six would have locked out every recovery code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:59:00 +02:00
parent b3f153a2dc
commit 2b7b2b64db
6 changed files with 366 additions and 6 deletions

View File

@@ -446,6 +446,170 @@
}
}
}
.otp {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-flex;
font-family: var(--font-mono);
clip-path: inset(-3.5px 3.5px -3.5px -3.5px);
border-radius: var(--radius-field);
font-size: 1.75rem;
gap: var(--otp-gap);
--input-color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
--input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
}
--otp-ch: 1ch;
@supports (font: -apple-system-body) {
--otp-ch: 0.618164em;
}
--otp-gap: calc(var(--otp-ch) * 0.5);
--otp-w: calc(var(--otp-ch) * 2);
--otp-size: calc(var(--size-field, 0.25rem) * 10);
--stride: calc(var(--otp-w) + var(--otp-gap));
> input {
pointer-events: none;
inset-inline-start: calc(0.25rem * 0);
z-index: 1;
margin: 0;
appearance: none;
border-style: var(--tw-border-style);
border-width: 0px;
background-color: transparent;
padding: 0;
outline-style: var(--tw-outline-style);
outline-width: 0px;
field-sizing: content;
padding-inline-start: calc(var(--otp-ch) * 0.5 + var(--border));
line-height: 1;
letter-spacing: calc(var(--stride) - var(--otp-ch));
font-variant-numeric: tabular-nums;
&::selection {
background-color: transparent;
color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
}
}
&:valid {
caret-color: transparent;
}
}
&:has( > span:nth-child(1)) {
width: calc(var(--stride) * 1);
}
&:has( > span:nth-child(2)) {
width: calc(var(--stride) * 2);
}
&:has( > span:nth-child(3)) {
width: calc(var(--stride) * 3);
}
&:has( > span:nth-child(4)) {
width: calc(var(--stride) * 4);
}
&:has( > span:nth-child(5)) {
width: calc(var(--stride) * 5);
}
&:has( > span:nth-child(6)) {
width: calc(var(--stride) * 6);
}
&:has( > span:nth-child(7)) {
width: calc(var(--stride) * 7);
}
&:has( > span:nth-child(8)) {
width: calc(var(--stride) * 8);
}
> span {
position: absolute;
display: flex;
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
transition: border-color 0.2s;
inline-size: var(--otp-w);
block-size: var(--otp-size);
background-color: var(--color-base-100);
border: var(--border) solid var(--input-color);
border-radius: inherit;
outline: 2px solid #0000;
outline-offset: 1px;
box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
@supports (color: color-mix(in lab, red, red)) {
box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
}
&:nth-child(1) {
left: 1px;
@supports (font: -apple-system-body) {
left: 2px;
}
@supports (-moz-appearance: none) {
left: 0px;
}
}
&:nth-child(2) {
left: calc(var(--stride) * 1);
transition-delay: 0.02s;
}
&:nth-child(3) {
left: calc(var(--stride) * 2);
transition-delay: 0.04s;
}
&:nth-child(4) {
left: calc(var(--stride) * 3);
transition-delay: 0.06s;
}
&:nth-child(5) {
left: calc(var(--stride) * 4);
transition-delay: 0.08s;
}
&:nth-child(6) {
left: calc(var(--stride) * 5);
transition-delay: 0.1s;
}
&:nth-child(7) {
left: calc(var(--stride) * 6);
transition-delay: 0.12s;
}
&:nth-child(8) {
left: calc(var(--stride) * 7);
transition-delay: 0.14s;
}
}
&:has(input:valid:focus) {
> span {
outline: 2px solid var(--input-color);
outline-offset: 1px;
}
&:after {
opacity: 0;
}
}
&:after {
flex-shrink: 0;
content: "";
width: var(--otp-w);
height: var(--otp-size);
border-radius: var(--radius-field);
outline: 2px solid #0000;
outline-offset: 1px;
z-index: 10;
margin-inline-start: calc(-1 * (var(--otp-gap) + var(--otp-ch) * 0.5 + var(--border)));
}
&:focus-within {
isolation: isolate;
--input-color: var(--color-base-content);
> span {
box-shadow: 0 1px var(--input-color);
@supports (color: color-mix(in lab, red, red)) {
box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000);
}
}
&:after {
outline-color: var(--input-color);
}
}
}
}
.collapse-arrow {
@layer daisyui.l1.l2 {
> .collapse-title:after {
@@ -2464,6 +2628,9 @@
.mt-8 {
margin-top: calc(var(--spacing) * 8);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
@@ -2986,6 +3153,9 @@
.pt-3 {
padding-top: calc(var(--spacing) * 3);
}
.pb-2 {
padding-bottom: calc(var(--spacing) * 2);
}
.text-center {
text-align: center;
}
@@ -3028,6 +3198,12 @@
font-size: 0.75rem;
}
}
.otp-lg {
@layer daisyui.l1.l2 {
font-size: 2rem;
--otp-size: calc(var(--size-field, 0.25rem) * 12);
}
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
@@ -3165,6 +3341,17 @@
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.input-lg {
@layer daisyui.l1.l2 {
--in-size-mul: 12;
--font-size-min: 1.125rem;
--spin-my: -3;
.floating-label:has(&) {
--top-mul: 6;
--font-size: 1.125rem;
}
}
}
.btn-accent {
@layer daisyui.l1.l2 {
--btn-color: var(--color-accent);