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.htmlenhanced with Option A:c-sectioncomponent slotsadd_purchase.htmldual-submit simplified — remove<tr><td>, use samec-buttonpattern asadd_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
- ~~
modal.htmlhas a missing<form>tag (line 13:</form>with no opening<form>)** — Resolved: rewritten as proper component with form wrapping support, body + footer slots, reusableclose_buttoncomponent. Ready for standardizing all inline modals later. - Delete confirmations are inconsistent — three different patterns (inline modal, full-page form, inline modal)
.errorlistCSS has fixedwidth: 300px— too narrow, breaks on mobile. No scoping needed: Django auto-applies.errorlistto form error output only, never used explicitly in templates.add_purchase.htmlhas<tr><td>in ac-slotthat renders inside a<div>— semantic mismatch. Decision: simplify dual-submit to matchadd_game.htmlpattern (use<c-button>only).#button-containerand.basic-buttonininput.css— legacy patterns, unused or dead code
P1: Layout & UX
- All add/edit forms are single-column — PurchaseForm (11 fields) and GameForm (7 fields) would benefit from multi-column
- No field grouping — related fields listed flat without visual hierarchy
- Stats year
<select>has no Flowbite styling - Search field is not wrapped in
<form method="get">— no native clear-on-Enter behavior
P2: Styling Consistency
- Status/device selectors use old Tailwind v3 patterns (
rounded-sm,shadow-2xs,border-gray-200without explicit color) navbar.htmlbuttons userounded-sminstead ofrounded-basesimple_table.htmlpagination buttons userounded-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
sectionsvariable (list of renderedc-sectionoutput) - If
sectionsprovided, render them; otherwise fall back to{{ form.as_div }}for simple forms - Keep
additional_rowslot 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, paddingselect— same base styling as inputstextarea— same base styling with adjusted padding
Files to clean up:
common/input.css: Remove.flowbite-inputclass entirely (lines 229-234)games/forms.py: Removeflowbite_input_widgetandflowbite_password_widget(lines 22-23)games/forms.py: Removewidget=fromLoginFormfields (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-modalcomponent with:modal_id,title,size="xl",backdrop_closevariables {{ 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.htmlvia<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-autoon 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.html→partials/statuschange_delete_confirmation.html): Adopt the same 2-view pattern as delete-game.- Add
delete_statuschange_confirmationview (GET → renders modal partial) + URL before the delete URL - Update
partials/history.html— addhx-get="{% url 'games:delete_statuschange_confirmation' change.id %}" hx-target="#global-modal-container"to the Delete link - Create new
partials/statuschange_delete_confirmation.htmlusing<c-modal>, same structure asdelete_game_confirmation.html(detail list, red warning text, same button layout,<c-gamestatus>badge for old status) - Modify
GameStatusChangeDeleteViewto only handle POST (remove its GET-rendered template) - Delete old
gamestatuschange_confirm_delete.htmlafter migration
- Add
- 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-lgwithrounded-base - Replace
shadow-2xswithshadow-xs - Standardize border colors with
border-default - Use
text-heading/text-bodyfor 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)
- CSS fixes (
input.css) — fix errorlist width, remove legacy.basic-button/#button-container, add select styles — add missingmodal.htmlrewrite<form>tag, conditional form wrapper ✓ Implemented (uses{{ slot }}cotton default slot, no{% partial %}tags;sizedefaults to"xl"with inline{% if %}mapping)- Delete confirmation standardization — 3 templates → all modal, same pattern (including GameStatusChange: full-page → modal)
- Search field enhancement — wrap in
<form method="get"> - Stats select styling — add Flowbite select classes
- Selector styling updates — gamestatus + sessiondevice selectors, consistent classes
Phase 2: c-section Component
- Create
c-sectioncomponent — title, columns, fields slots - Update
cotton/layouts/add.html— supportsectionsvariable, fallback toform.as_div
Phase 3: Form Layout Overhaul (largest change)
GameForm— section headers + 2-col flexbox (add_game.html)PlayEventForm— section headers + 2-col flexboxPurchaseForm— section headers + 2/3-col flexbox + simplify dual-submit (add_purchase.html)SessionForm— section headers + flexbox for timestamps+duration (custom rendering already exists)- Simple forms —
add_platform.html,add_device.htmlget section headers (single column)
Testing Strategy
- Run
make testafter Phase 1 changes to verify nothing broke tests/test_paths_return_200.py— URL-level smoke tests (186 tests). All views must have atest_*_returns_200test. Adding new views requires a corresponding test to preventTemplateDoesNotExistregressions.- 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 A —
c-sectioncotton component withtitleandcolumnsslots - add_purchase.html dual-submit? → Simplify — remove
<tr><td>, use same<c-button>pattern asadd_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
@applyininput.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.cssmake dev— watch mode: Tailwind rebuilds automatically on everyinput.csssave
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.