From 17c5fdb8a883e45f9e9bbec7b8c25d740334f21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 9 Jun 2026 20:46:03 +0200 Subject: [PATCH] docs: add implementation plan for unifying form checkboxes --- .../plans/2026-06-09-unify-form-checkboxes.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-unify-form-checkboxes.md diff --git a/docs/superpowers/plans/2026-06-09-unify-form-checkboxes.md b/docs/superpowers/plans/2026-06-09-unify-form-checkboxes.md new file mode 100644 index 0000000..0797120 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-unify-form-checkboxes.md @@ -0,0 +1,177 @@ +# Unify Form Checkboxes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Unify all Django form checkboxes across the codebase by routing them through our new Python `Checkbox` primitive. +**Architecture:** +1. Modify `Checkbox` and `Radio` primitives in `common/components/primitives.py` to support headless (label-less) rendering when `label` is `None`, so they can be injected into Django's native `form.as_div()` rendering without duplicating labels. +2. Create a `PrimitiveCheckboxWidget` in `games/forms.py` that extends `forms.CheckboxInput` but renders using our `Checkbox` Python component. +3. Create a `PrimitiveWidgetsMixin` in `games/forms.py` that automatically applies the `PrimitiveCheckboxWidget` to all `forms.BooleanField` instances in a form. Add this mixin to all ModelForms. + +**Tech Stack:** Python, Django Forms, HTML. + +--- + +### Task 1: Update Primitives for Headless Rendering + +**Files:** +- Modify: `common/components/primitives.py` +- Modify: `tests/test_components.py` + +- [ ] **Step 1: Write a failing test for headless rendering** +In `tests/test_components.py`, add a test to `ComponentPrimitivesTest`: +```python + def test_checkbox_headless(self): + html = Checkbox(name="test-headless", label=None, checked=True) + self.assertNotIn(' SafeText: + """A filter-agnostic Checkbox component.""" + attributes = attributes or [] + input_attrs = [ + ("name", name), + ("value", value), + ("class", "rounded border-default-medium bg-neutral-secondary-medium text-brand focus:ring-brand"), + ] + attributes + if checked: + input_attrs.append(("checked", "true")) + + input_el = Input(type="checkbox", attributes=input_attrs) + if label is None: + return input_el + + return Label( + attributes=[("class", "flex items-center gap-2 text-sm text-heading cursor-pointer")], + children=[input_el, label], + ) + +def Radio( + name: str, + label: str | None = None, + checked: bool = False, + value: str = "", + attributes: list[HTMLAttribute] | None = None, +) -> SafeText: + """A filter-agnostic Radio component.""" + attributes = attributes or [] + input_attrs = [ + ("name", name), + ("value", value), + ("class", "rounded-full border-default-medium bg-neutral-secondary-medium text-brand focus:ring-brand"), + ] + attributes + if checked: + input_attrs.append(("checked", "true")) + + input_el = Input(type="radio", attributes=input_attrs) + if label is None: + return input_el + + return Label( + attributes=[("class", "flex items-center gap-1.5 text-sm text-heading cursor-pointer")], + children=[input_el, label], + ) +``` + +- [ ] **Step 4: Run test to verify it passes** +Run: `pytest tests/test_components.py -k ComponentPrimitivesTest` +Expected: PASS + +- [ ] **Step 5: Commit** +Run: +```bash +git add common/components/primitives.py tests/test_components.py +git commit -m "refactor: allow Checkbox and Radio primitives to render headlessly without labels" +``` + +--- + +### Task 2: Create Django Widget Adapter and Mixin + +**Files:** +- Modify: `games/forms.py` + +- [ ] **Step 1: Write the Widget and Mixin implementations** +At the top of `games/forms.py`, import `Checkbox` and implement `PrimitiveCheckboxWidget` and `PrimitiveWidgetsMixin`. +```python +from common.components.primitives import Checkbox + +class PrimitiveCheckboxWidget(forms.CheckboxInput): + """Adapts Django's CheckboxInput to use our Checkbox component.""" + def render(self, name, value, attrs=None, renderer=None): + final_attrs = self.build_attrs(self.attrs, attrs) + checked = self.check_test(value) + attributes = [(k, str(v)) for k, v in final_attrs.items() if k not in ("type", "name", "value", "checked")] + + # Django uses boolean values differently for checkboxes, we omit value if empty + return str(Checkbox( + name=name, + label=None, + checked=checked, + value=str(value) if value else "1", + attributes=attributes + )) + +class PrimitiveWidgetsMixin: + """Automatically applies primitive custom widgets to native Django form fields.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name, field in self.fields.items(): + if isinstance(field, forms.BooleanField): + field.widget = PrimitiveCheckboxWidget() + # Maintain the field's explicit required status (usually False for booleans) +``` + +- [ ] **Step 2: Apply the Mixin to all Forms** +In `games/forms.py`, update all the ModelForm classes to inherit from `PrimitiveWidgetsMixin` as the **first** base class (before `forms.ModelForm`). +Example: +```python +class SessionForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class PurchaseForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class GameForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class PlatformForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class DeviceForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class PlayEventForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... + +class GameStatusChangeForm(PrimitiveWidgetsMixin, forms.ModelForm): + # ... +``` + +- [ ] **Step 3: Test Django Form Rendering** +Run the full test suite to ensure forms still validate properly and render without error. +Run: `pytest` +Expected: PASS + +- [ ] **Step 4: Commit** +Run: +```bash +git add games/forms.py +git commit -m "feat: replace all form BooleanFields with PrimitiveCheckboxWidget via mixin" +``` \ No newline at end of file