Files
2026-06-07 07:31:28 +02:00

20 KiB

Form Overhaul Plan

Last updated: 2026-05-12 Status: Decided — awaiting implementation

Decisions made:

  • All forms (simple and complex) get section headers for consistency
  • Two-column layout uses flexbox (auto-reflow on different screen sizes)
  • cotton/layouts/add.html enhanced with Option A: c-section component slots
  • add_purchase.html dual-submit simplified — remove <tr><td>, use same c-button pattern as add_game.html
  • GameStatusChange delete confirmation converted to modal (via HTMX trigger)

Goal

Modernize all forms and form-like elements to align with Flowbite design, improve visual consistency, and adopt responsive multi-column layouts for complex forms.


Current State Analysis

Form Pages (add/edit)

All use cotton/layouts/add.html — single column, max-w-xl, form.as_div:

Page Form Fields Complexity
Game GameForm 7 fields: name, sort_name, platform, year, year_orig, status, mastered, wikidata Medium
Purchase PurchaseForm 11 fields: games, platform, dates, price, currency, type, ownership, related, infinite, name High
Session SessionForm 8 fields: game, timestamps, duration, emulated, device, note, checkbox (custom rendering) High
Platform PlatformForm 3 fields: name, icon, group Low
Device DeviceForm 2 fields: name, type Low
PlayEvent PlayEventForm 5 fields: game, dates, note, checkbox Low
GameStatusChange GameStatusChangeForm 4 fields Low

Other Form-Like Elements

Element Template Notes
Login registration/login.html Flowbite card, already good
Search cotton/search_field.html Reusable, already good
Delete Game partials/delete_game_confirmation.html Inline modal, inconsistent button layout
Delete PlayEvent gamestatuschange_confirm_delete.html Full-page form, no modal
Refund Purchase partials/refund_purchase_confirmation.html Inline modal, inconsistent button layout
Stats Year Select stats.html Manual <select>, no Flowbite styling
Status Selector partials/gamestatus_selector.html Alpine.js dropdown, old Tailwind classes
Device Selector partials/sessiondevice_selector.html Alpine.js dropdown, old Tailwind classes

Issues to Fix

P0: Broken/Inconsistent

  1. ~~modal.html has a missing <form> tag (line 13: </form> with no opening <form>)** — Resolved: rewritten as proper component with form wrapping support, body + footer slots, reusable close_button component. Ready for standardizing all inline modals later.
  2. Delete confirmations are inconsistent — three different patterns (inline modal, full-page form, inline modal)
  3. .errorlist CSS has fixed width: 300px — too narrow, breaks on mobile. No scoping needed: Django auto-applies .errorlist to form error output only, never used explicitly in templates.
  4. add_purchase.html has <tr><td> in a c-slot that renders inside a <div> — semantic mismatch. Decision: simplify dual-submit to match add_game.html pattern (use <c-button> only).
  5. #button-container and .basic-button in input.css — legacy patterns, unused or dead code

P1: Layout & UX

  1. All add/edit forms are single-column — PurchaseForm (11 fields) and GameForm (7 fields) would benefit from multi-column
  2. No field grouping — related fields listed flat without visual hierarchy
  3. Stats year <select> has no Flowbite styling
  4. Search field is not wrapped in <form method="get"> — no native clear-on-Enter behavior

P2: Styling Consistency

  1. Status/device selectors use old Tailwind v3 patterns (rounded-sm, shadow-2xs, border-gray-200 without explicit color)
  2. navbar.html buttons use rounded-sm instead of rounded-base
  3. simple_table.html pagination buttons use rounded-s-lg/rounded-e-lg — could be simplified

Proposed Improvements

1. Two-Column Layout for Complex Forms (Flexbox)

Scope: GameForm, PurchaseForm, PlayEventForm, SessionForm

Use flexbox with wrap behavior so fields auto-reflow on different screen sizes. No fixed column count — fields sit side-by-side on md:+ and wrap naturally on smaller screens.

GameForm Layout

┌──────────────────────────────────┐
│ Game Details                     │
│ ┌──────────────────┬───────────┐ │
│ │ Name             │ Platform  │ │
│ │ Sort Name        │ Year      │ │
│ │ Original Year    │ Wikidata  │ │
│ └──────────────────┴───────────┘ │
│ Status                           │
│ ┌──────────────────┬───────────┐ │
│ │ Status           │ Mastered  │ │
│ └──────────────────┴───────────┘ │
│         [Submit]                 │
└──────────────────────────────────┘

PurchaseForm Layout (simplified)

┌──────────────────────────────────────────┐
│ Purchase Details                         │
│ ┌──────────────────────┬───────────────┐ │
│ │ Games (multi-select) │ Platform      │ │
│ │ Type                 │ Ownership     │ │
│ │ Name                 │ Related Purch │ │
│ └──────────────────────┴───────────────┘ │
│ Dates                    │ Price          │
│ ┌───────────────┬──────┴───────────────┐ │
│ │ Date Purch    │ Price         Curr   │ │
│ │ Date Refund   │ Infinite [ ]         │ │
│ └───────────────┴──────────────────────┘ │
│         [Submit] [Submit + Session]      │
└──────────────────────────────────────────┘

Implementation: c-section component accepts columns="2" (or "3") which applies flex flex-wrap gap-4 [&>div]:w-[calc(50%-0.5rem)] on md+ screens. Each field wraps in a <div> inside the section slot.

Decision: Dual-submit in add_purchase.html simplified — remove <tr><td>, use same <c-button> pattern as add_game.html.

2. Field Grouping with Card Sections

Decision: ALL forms get section headers for consistency (not just complex forms).

Group related fields with section headings and subtle borders/backgrounds:

<c-section title="Game Details" columns="2">
  {{ form.name }}
  {{ form.platform }}
  {{ form.sort_name }}
  {{ form.year_released }}
</c-section>

Each section renders as:

<fieldset class="form-section p-5 border-t border-default-medium bg-neutral-primary-soft/30 first-of-type:border-t-0 first-of-type:pt-0">
  <h3 class="text-sm font-medium text-heading uppercase mb-4">Section Title</h3>
  <div class="flex flex-wrap gap-4">
    <!-- fields in <div> wrappers, each taking calc(50% - 0.5rem) on md+ -->
  </div>
</fieldset>

Each section gets:

  • Subtle background (bg-neutral-primary-soft/30)
  • Top border with spacing (border-t border-default-medium)
  • Section heading (text-sm font-medium text-heading uppercase mb-4)
  • Flexbox gap for responsive field reflow

1b. c-section Component Specification

New cotton component for the cotton/ directory:

# games/templates/cotton/section.py (or inline in components.py)
from common.components import Div

def Section(title: str = "", columns: str = "1", children: str = "") -> SafeText:
    """Renders a form field section with optional multi-column flexbox layout.
    
    Args:
        title: Section heading (renders as uppercase label)
        columns: "1" (default), "2", or "3" — target column count on md+ screens
        children: Field markup (each field wrapped in <div> for flex wrapping)
    """
    col_class = {
        "1": "flex flex-col",
        "2": "flex flex-wrap gap-4 [&>div]:w-[calc(50%-0.5rem)]",
        "3": "flex flex-wrap gap-4 [&>div]:w-[calc(33.333%-0.67rem)]",
    }.get(columns, "flex flex-col")
    
    return Div(
        cls=f"form-section p-5 border-t border-default-medium bg-neutral-primary-soft/30 first-of-type:border-t-0 first-of-type:pt-0",
        children=f"""
            <h3 class="text-sm font-medium text-heading uppercase mb-4">{title}</h3>
            <div class="{col_class}">{children}</div>
        """
    )

Template usage:

{# add_game.html #}
<c-layouts.add title="New Game">
  <c-section title="Game Details" columns="2">
    <div>{{ form.name }}</div>
    <div>{{ form.platform }}</div>
    <div>{{ form.sort_name }}</div>
    <div>{{ form.year_released }}</div>
    <div>{{ form.original_year_released }}</div>
    <div>{{ form.wikidata }}</div>
  </c-section>
  <c-section title="Status" columns="2">
    <div>{{ form.status }}</div>
    <div>{{ form.mastered }}</div>
  </c-section>
</c-layouts.add>

cotton/layouts/add.html changes:

  • Remove hardcoded {{ form.as_div }} rendering
  • Accept optional sections variable (list of rendered c-section output)
  • If sections provided, render them; otherwise fall back to {{ form.as_div }} for simple forms
  • Keep additional_row slot for dual-submit buttons

3. CSS/Style Fixes

input.css changes:

/* Update errorlist */
.errorlist {
  @apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-full max-w-xl;  /* was w-[300px] */
}

/* Remove: #button-container, .basic-button — unused legacy */

/* Remove: .flowbite-input — custom class is code smell with Tailwind */
/* Remove: flowbite-input @apply block (line 229-234) */

/* Add Flowbite styling for select in stats */
#yearSelect {
  @apply bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand;
}

Important: The styling previously provided by .flowbite-input must be preserved. The element-level @apply rules for input, select, and textarea in input.css (lines 209-219) already provide equivalent styling. These rules automatically apply to all form inputs without needing custom classes:

  • input:not([type="checkbox"]) — background, border, text, radius, focus ring, padding
  • select — same base styling as inputs
  • textarea — same base styling with adjusted padding

Files to clean up:

  • common/input.css: Remove .flowbite-input class entirely (lines 229-234)
  • games/forms.py: Remove flowbite_input_widget and flowbite_password_widget (lines 22-23)
  • games/forms.py: Remove widget= from LoginForm fields (lines 28, 32) — login template uses explicit Tailwind classes already

Rewrite modal.html:

  • Remove stray </form> tag and restructure as a proper cotton component
  • New c-modal component with: modal_id, title, size="xl", backdrop_close variables
  • {{ slot }} (cotton default slot) for body content — passed as children of <c-modal>, no block tags needed
  • {{ footer }} (optional named slot via <c-slot name="footer">) for non-form buttons
  • Reusable cotton/close_button.html via <c-close-button />
  • Size mapping via inline {% if %}: {% if size == 'sm' %}max-w-sm{% elif size == 'lg' %}max-w-lg{% else %}max-w-xl{% endif %}
  • Horizontal centering: mx-auto on inner container (matching old modal pattern)
  • Click-to-dismiss backdrop with event.stopPropagation() on inner container
  • Flowbite-style styling: rounded-lg shadow, bg-white dark:bg-gray-800, sm:p-5

4. Unify Delete Confirmations (All Modal)

Decision: GameStatusChange delete confirmation converted from full-page to modal. All three use the same modal pattern.

Target: All confirmation modals use the same pattern:

<div class="fixed inset-0 bg-black/70 dark:bg-gray-600/50 ...">
  <div class="relative mx-auto p-6 bg-white dark:bg-gray-900 rounded-lg shadow-lg max-w-md w-full">
    <h2 class="text-xl font-medium text-center">Confirm Action</h2>
    <p class="text-center mt-4 text-sm text-body">Are you sure...?</p>
    {% if details %}
    <ul class="text-center mt-2 text-sm text-body list-disc list-inside">
      <li>{{ detail }}</li>
    </ul>
    {% endif %}
    <p class="text-center mt-3 text-sm font-medium text-red-600">This action cannot be undone.</p>
    <div class="flex gap-3 mt-6">
      <c-button color="red" class="w-full" type="submit">Delete</c-button>
      <c-button color="gray" class="w-full">Cancel</c-button>
    </div>
  </div>
</div>
  • Delete Game (partials/delete_game_confirmation.html): Update template to match standard pattern
  • Delete StatusChange (gamestatuschange_confirm_delete.htmlpartials/statuschange_delete_confirmation.html): Adopt the same 2-view pattern as delete-game.
    • Add delete_statuschange_confirmation view (GET → renders modal partial) + URL before the delete URL
    • Update partials/history.html — add hx-get="{% url 'games:delete_statuschange_confirmation' change.id %}" hx-target="#global-modal-container" to the Delete link
    • Create new partials/statuschange_delete_confirmation.html using <c-modal>, same structure as delete_game_confirmation.html (detail list, red warning text, same button layout, <c-gamestatus> badge for old status)
    • Modify GameStatusChangeDeleteView to only handle POST (remove its GET-rendered template)
    • Delete old gamestatuschange_confirm_delete.html after migration
  • Refund Purchase (partials/refund_purchase_confirmation.html): Update template to match standard pattern

5. Search Form Enhancement

Wrap search_field.html in proper <form method="get">:

<form class="max-w-md mx-auto" method="get" x-data x-on:keydown.escape="this.querySelector('input').value=''; this.submit()">
  <!-- input + button -->
</form>

This enables:

  • Native form submission on Enter
  • Potential for "clear all" functionality
  • Proper browser form autofill behavior

6. Status/Device Selector Styling

Update Alpine.js dropdowns to use consistent button classes:

  • Replace rounded-lg with rounded-base
  • Replace shadow-2xs with shadow-xs
  • Standardize border colors with border-default
  • Use text-heading / text-body for dark mode compatibility

Templates That Need Changes

Template Change Effort
cotton/layouts/add.html Add c-section component support (title, columns, fields slots) Medium
add_game.html Multi-column flexbox layout, section headers Medium
add_purchase.html Multi-column flexbox layout, simplify dual-submit, section headers High
add_session.html Flexbox layout for timestamps+duration, section headers Low
add_playevent.html Flexbox layout, section headers Low
add_platform.html Section headers (was flat single-column) Low
add_device.html Section headers (was flat single-column) Low
partials/delete_game_confirmation.html Standardize to shared modal pattern Low
partials/refund_purchase_confirmation.html Standardize to shared modal pattern Low
partials/statuschange_delete_confirmation.html New — adopt same 2-view pattern as delete-game (modal, <c-modal>, HTMX triggers) Medium
gamestatuschange_confirm_delete.html Delete (replaced by new partial) Trivial
cotton/modal.html Fix missing <form> tag Low
stats.html Add Flowbite select styling Low
partials/gamestatus_selector.html Update button classes Low
partials/sessiondevice_selector.html Update button classes Low
cotton/search_field.html Wrap in <form method="get"> Low
common/input.css Remove legacy, fix errorlist, add select styles Low

Implementation Order

Phase 1: Quick Wins (low risk, no breaking changes)

  1. CSS fixes (input.css) — fix errorlist width, remove legacy .basic-button / #button-container, add select styles
  2. modal.html rewrite — add missing <form> tag, conditional form wrapper ✓ Implemented (uses {{ slot }} cotton default slot, no {% partial %} tags; size defaults to "xl" with inline {% if %} mapping)
  3. Delete confirmation standardization — 3 templates → all modal, same pattern (including GameStatusChange: full-page → modal)
  4. Search field enhancement — wrap in <form method="get">
  5. Stats select styling — add Flowbite select classes
  6. Selector styling updates — gamestatus + sessiondevice selectors, consistent classes

Phase 2: c-section Component

  1. Create c-section component — title, columns, fields slots
  2. Update cotton/layouts/add.html — support sections variable, fallback to form.as_div

Phase 3: Form Layout Overhaul (largest change)

  1. GameForm — section headers + 2-col flexbox (add_game.html)
  2. PlayEventForm — section headers + 2-col flexbox
  3. PurchaseForm — section headers + 2/3-col flexbox + simplify dual-submit (add_purchase.html)
  4. SessionForm — section headers + flexbox for timestamps+duration (custom rendering already exists)
  5. Simple formsadd_platform.html, add_device.html get section headers (single column)

Testing Strategy

  • Run make test after Phase 1 changes to verify nothing broke
  • tests/test_paths_return_200.py — URL-level smoke tests (186 tests). All views must have a test_*_returns_200 test. Adding new views requires a corresponding test to prevent TemplateDoesNotExist regressions.
  • CSS changes do not require test changes (no test coverage for rendering), but visual verification is recommended

Open Questions

  • Simple forms section headers? → All forms get section headers for consistency
  • CSS Grid or Flexbox? → Flexbox — auto-reflow on different screen sizes
  • add.html layout variable? → Option Ac-section cotton component with title and columns slots
  • add_purchase.html dual-submit? → Simplify — remove <tr><td>, use same <c-button> pattern as add_game.html
  • GameStatusChange modal or full-page? → Modal — trigger via HTMX, same pattern as delete-game
  • .flowbite-input class? → Remove entirely — rely on element-level @apply in input.css

Decision Summary

Question Decision
Section headers on simple forms Yes, all forms get them
Layout approach for multi-column Flexbox with wrap
Layout mechanism in add.html Option A: c-section cotton component
Purchase dual-submit Simplify — single submit button, same as Game
GameStatusChange delete Convert to modal (HTMX-triggered)
.flowbite-input class Remove — preserve styling via element-level @apply in input.css
modal.html component Rewrite with form wrapping, body + footer slots, reusable close button ✓ Implemented

Build Step

After any CSS changes to common/input.css, the compiled output must be rebuilt:

  • make css — one-shot build: npx @tailwindcss/cli -i ./common/input.css -o ./games/static/base.css
  • make dev — watch mode: Tailwind rebuilds automatically on every input.css save

Running make dev is sufficient for development since it concurrently runs Django and the CSS watcher. Only use make css if you only want to rebuild CSS without starting the dev server.

Important: Legacy CSS removals (.basic-button, #button-container, .flowbite-input) will only take effect in the browser after a rebuild. The old compiled base.css will still contain them until rebuilt.