02798f8858
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>
49 lines
1.5 KiB
Python
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",
|
|
)
|