Files
timetracker/games/views/auth.py
T
lukas 02798f8858 Own all form styling in components; remove form CSS from input.css
Form controls were styled "at a distance": Django renders bare
<input>/<select>/<textarea>/<label>, so input.css reached in with ID-scoped
#add-form descendant rules plus a global form *:disabled rule and .errorlist.
The #add-form ID specificity forced state rules to climb, needed
:not([data-search-select-search]) carve-outs, and broke on markup changes — it
surfaced as the add_purchase Name/related_game fields not reading as disabled.

Components now own all form styling via utilities on the elements themselves:

- PrimitiveWidgetsMixin stamps INPUT/SELECT/TEXTAREA_CLASS (incl. disabled:
  variants) onto native widgets by type, skipping SearchSelect (self-styled)
  and checkboxes.
- New FormFields(form, *, extras=...) renders label + control + errors + row
  layout with their own classes (replaces form.as_div()); the <form> owns its
  flex layout. extras appends a node into a named field's row (session
  timestamp buttons).
- AddForm/purchase/session render via FormFields; login too — a new
  LoginForm(PrimitiveWidgetsMixin, AuthenticationForm) styles its inputs and
  auth.py renders it via FormFields + a StyledButton (was as_table).
- input.css loses the entire #add-form block, the global :disabled rule, and
  .errorlist. State (disabled:) now lives on the element — no specificity wars,
  no carve-outs, robust to markup edits.

Tests: error rendering uses the component class (not .errorlist); add-form
labels/inputs carry their own classes; e2e login fixtures click the Login
button by text (submit is now a <button>); Name disabled cursor asserted.
CLAUDE.md documents the no-styling-at-a-distance + FormFields conventions.

513 passed; lint/format/ts-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:31:53 +02:00

49 lines
1.5 KiB
Python

"""Authentication views rendered with the Python layout (replaces
registration/login.html)."""
from django.contrib.auth import views as auth_views
from django.http import HttpResponse
from common.components import CsrfInput, Div, Element, FormFields, Node, StyledButton
from common.layout import render_page
from games.forms import LoginForm
def _login_content(form, request) -> Node:
return Div(
[("class", "flex items-center flex-col")],
[
Element(
"h2",
attributes=[("class", "text-3xl text-white mb-8")],
children=["Please log in to continue"],
),
Element(
"form",
attributes=[
("method", "post"),
("class", "flex flex-col gap-3 w-full max-w-sm"),
],
children=[
CsrfInput(request),
FormFields(form),
StyledButton([], "Login", type="submit"),
],
),
],
)
class LoginView(auth_views.LoginView):
"""Django's LoginView, but the page body is built in Python and the form is
our `LoginForm` so its inputs self-style like every other form."""
authentication_form = LoginForm
def render_to_response(self, context, **response_kwargs) -> HttpResponse:
return render_page(
self.request,
_login_content(context["form"], self.request),
title="Login",
)