From 5e7ff8f3aff626528b2231137b00dc3fd1c820a6 Mon Sep 17 00:00:00 2001 From: Francis Secada Date: Wed, 19 Aug 2026 20:39:31 -0400 Subject: [PATCH 1/2] feat(primitives): attrs passthrough on Icon, Badge, Box, CheckboxGroup (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rolls out the attrs-passthrough shape from Button (#72/#73) and Select/ Textarea/FormField (#76/#77) to the remaining interactive-set primitives from #70: Icon, Badge, Box, and CheckboxGroup, across both engines and all five themes. CheckboxGroup departs from the established "control, not wrapper" rule: it renders N checkbox inputs from `choices` with no single control to target, so attrs lands on the outer wrapper element instead — a deliberate design decision, tested and documented. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs --- docs/components.md | 3 + docs/primitives.md | 14 +- src/cf_ui/primitives.py | 8 + .../cotton/_themes/bootstrap/badge.html | 2 + .../cotton/_themes/bootstrap/box.html | 2 + .../_themes/bootstrap/checkbox-group.html | 6 +- .../cotton/_themes/bootstrap/icon.html | 2 + .../templates/cotton/_themes/bulma/badge.html | 2 + .../templates/cotton/_themes/bulma/box.html | 6 +- .../cotton/_themes/bulma/checkbox-group.html | 6 +- .../templates/cotton/_themes/bulma/icon.html | 2 + .../templates/cotton/_themes/daisy/badge.html | 2 + .../templates/cotton/_themes/daisy/box.html | 2 + .../cotton/_themes/daisy/checkbox-group.html | 6 +- .../templates/cotton/_themes/daisy/icon.html | 2 + .../cotton/_themes/fomantic/badge.html | 2 + .../cotton/_themes/fomantic/box.html | 2 + .../_themes/fomantic/checkbox-group.html | 6 +- .../cotton/_themes/fomantic/icon.html | 2 + .../cotton/_themes/foundation/badge.html | 2 + .../cotton/_themes/foundation/box.html | 2 + .../_themes/foundation/checkbox-group.html | 6 +- .../cotton/_themes/foundation/icon.html | 2 + src/cf_ui/templates/cotton/cf/badge.html | 7 +- src/cf_ui/templates/cotton/cf/box.html | 6 +- .../templates/cotton/cf/checkbox-group.html | 1 + src/cf_ui/templates/cotton/cf/icon.html | 7 +- .../templates/jinja/bootstrap/Badge.jinja | 4 +- src/cf_ui/templates/jinja/bootstrap/Box.jinja | 4 +- .../jinja/bootstrap/CheckboxGroup.jinja | 8 +- .../templates/jinja/bootstrap/Icon.jinja | 4 +- src/cf_ui/templates/jinja/bulma/Badge.jinja | 4 +- src/cf_ui/templates/jinja/bulma/Box.jinja | 8 +- .../templates/jinja/bulma/CheckboxGroup.jinja | 8 +- src/cf_ui/templates/jinja/bulma/Icon.jinja | 4 +- src/cf_ui/templates/jinja/daisy/Badge.jinja | 4 +- src/cf_ui/templates/jinja/daisy/Box.jinja | 4 +- .../templates/jinja/daisy/CheckboxGroup.jinja | 8 +- src/cf_ui/templates/jinja/daisy/Icon.jinja | 4 +- .../templates/jinja/fomantic/Badge.jinja | 4 +- src/cf_ui/templates/jinja/fomantic/Box.jinja | 4 +- .../jinja/fomantic/CheckboxGroup.jinja | 8 +- src/cf_ui/templates/jinja/fomantic/Icon.jinja | 4 +- .../templates/jinja/foundation/Badge.jinja | 4 +- .../templates/jinja/foundation/Box.jinja | 4 +- .../jinja/foundation/CheckboxGroup.jinja | 8 +- .../templates/jinja/foundation/Icon.jinja | 4 +- .../test_checkbox_group_attrs_passthrough.py | 99 ++++++++++++ .../test_icon_badge_box_attrs_passthrough.py | 134 +++++++++++++++ .../test_checkbox_group_attrs_passthrough.py | 114 +++++++++++++ .../test_icon_badge_box_attrs_passthrough.py | 152 ++++++++++++++++++ 51 files changed, 675 insertions(+), 38 deletions(-) create mode 100644 tests/unit/cotton/test_checkbox_group_attrs_passthrough.py create mode 100644 tests/unit/cotton/test_icon_badge_box_attrs_passthrough.py create mode 100644 tests/unit/jinja/test_checkbox_group_attrs_passthrough.py create mode 100644 tests/unit/jinja/test_icon_badge_box_attrs_passthrough.py diff --git a/docs/components.md b/docs/components.md index e9e3bdf..fd7fab5 100644 --- a/docs/components.md +++ b/docs/components.md @@ -46,6 +46,7 @@ an unstyled element. | `variant` | `"neutral"` | | | `size` | `"normal"` | Inert on Bootstrap and Foundation | | `extra_class` | `""` | | +| `attrs` | `{}` | Extra HTML attributes — see [Attribute passthrough](primitives.md#attribute-passthrough) | ### `Cf:Heading` / `` @@ -72,6 +73,7 @@ an unstyled element. | `size` | `"normal"` | Inert on Foundation | | `label` | `""` | Empty ⇒ `aria-hidden`. Non-empty ⇒ `role="img"` + that name | | `extra_class` | `""` | | +| `attrs` | `{}` | Extra HTML attributes — see [Attribute passthrough](primitives.md#attribute-passthrough) | cf-ui ships no icons — put your own `` or `` in the slot. @@ -124,6 +126,7 @@ A labelled input with error display. | `selected` | `[]` | List of checked values | | `error` | `""` | | | `extra_class` / `control_class` | `""` | | +| `attrs` | `{}` | Extra HTML attributes, on the **wrapper** — not a per-choice `` on every theme, with no `role` and no ARIA. `role="status"` @@ -370,6 +376,7 @@ print and high-contrast stylesheets, and its announcement is inconsistent. | `size` | `"normal"` | Inert on Foundation; mixed scale on Bootstrap | | `label` | `""` | Empty ⇒ decorative. Non-empty ⇒ the accessible name | | `extra_class` / `class` | `""` | | +| `attrs` | `{}` | See [Attribute passthrough](#attribute-passthrough) above. `role`, `aria-label` and `aria-hidden` are reserved — pass `label` instead | | slot | — | Your ``, ``, or `` | The accessibility decision is most of what this component buys you, and it is @@ -401,6 +408,7 @@ choose its consumers' icon vendor. |---|---|---| | `variant` | `"neutral"` | Inert on Bulma — see below | | `extra_class` / `class` | `""` | | +| `attrs` | `{}` | See [Attribute passthrough](#attribute-passthrough) above | | slot | — | Arbitrary content | `box` is a plain bordered or elevated container: one element, no imposed diff --git a/src/cf_ui/primitives.py b/src/cf_ui/primitives.py index e2bd68e..a026531 100644 --- a/src/cf_ui/primitives.py +++ b/src/cf_ui/primitives.py @@ -813,6 +813,14 @@ def validate(component: str, **axes: Any) -> str: "aria-describedby", } ), + "icon": frozenset({"class", "role", "aria-label", "aria-hidden"}), + "badge": frozenset({"class"}), + "box": frozenset({"class"}), + # Rendered on the field wrapper, not a per-choice checkbox — see + # docs/primitives.md: unlike the other form controls, checkbox-group has + # no single control to target (it renders N inputs from `choices`), so + # `attrs` lands on the one element the call site actually identifies. + "checkbox-group": frozenset({"class"}), } diff --git a/src/cf_ui/templates/cotton/_themes/bootstrap/badge.html b/src/cf_ui/templates/cotton/_themes/bootstrap/badge.html index fb433d6..0f759f1 100644 --- a/src/cf_ui/templates/cotton/_themes/bootstrap/badge.html +++ b/src/cf_ui/templates/cotton/_themes/bootstrap/badge.html @@ -1,3 +1,5 @@ +{% load cf_ui %} {{ slot }} diff --git a/src/cf_ui/templates/cotton/_themes/bootstrap/box.html b/src/cf_ui/templates/cotton/_themes/bootstrap/box.html index 92f8e35..76b4fbd 100644 --- a/src/cf_ui/templates/cotton/_themes/bootstrap/box.html +++ b/src/cf_ui/templates/cotton/_themes/bootstrap/box.html @@ -1,7 +1,9 @@ +{% load cf_ui %} {% comment %}Bootstrap has no box component. `card` would be wrong — it imposes a header/body/footer structure the caller cannot opt out of — so this is the utility composition Bootstrap's own docs reach for. `neutral` maps to nothing because `.border` already sets the default border colour.{% endcomment %}
{{ slot }}
diff --git a/src/cf_ui/templates/cotton/_themes/bootstrap/checkbox-group.html b/src/cf_ui/templates/cotton/_themes/bootstrap/checkbox-group.html index b5b70f8..6ef628f 100644 --- a/src/cf_ui/templates/cotton/_themes/bootstrap/checkbox-group.html +++ b/src/cf_ui/templates/cotton/_themes/bootstrap/checkbox-group.html @@ -1,4 +1,8 @@ -
+{% load cf_ui %} +
{% for choice in choices %} diff --git a/src/cf_ui/templates/cotton/_themes/bootstrap/icon.html b/src/cf_ui/templates/cotton/_themes/bootstrap/icon.html index f01e5de..2680ebb 100644 --- a/src/cf_ui/templates/cotton/_themes/bootstrap/icon.html +++ b/src/cf_ui/templates/cotton/_themes/bootstrap/icon.html @@ -1,4 +1,6 @@ +{% load cf_ui %} diff --git a/src/cf_ui/templates/cotton/_themes/bulma/badge.html b/src/cf_ui/templates/cotton/_themes/bulma/badge.html index 358ff47..6c8449a 100644 --- a/src/cf_ui/templates/cotton/_themes/bulma/badge.html +++ b/src/cf_ui/templates/cotton/_themes/bulma/badge.html @@ -1,3 +1,5 @@ +{% load cf_ui %} {{ slot }} diff --git a/src/cf_ui/templates/cotton/_themes/bulma/box.html b/src/cf_ui/templates/cotton/_themes/bulma/box.html index 41bad2b..7a09f51 100644 --- a/src/cf_ui/templates/cotton/_themes/bulma/box.html +++ b/src/cf_ui/templates/cotton/_themes/bulma/box.html @@ -1,4 +1,8 @@ +{% load cf_ui %} {% comment %}Bulma ships no colour modifier for `.box`, so `variant` is inert here. `has-background-*` exists but sets a saturated background without touching text colour, which would render unreadable rather than themed.{% endcomment %} -
{{ slot }}
+
{{ slot }}
diff --git a/src/cf_ui/templates/cotton/_themes/bulma/checkbox-group.html b/src/cf_ui/templates/cotton/_themes/bulma/checkbox-group.html index 2fa07ef..9289def 100644 --- a/src/cf_ui/templates/cotton/_themes/bulma/checkbox-group.html +++ b/src/cf_ui/templates/cotton/_themes/bulma/checkbox-group.html @@ -1,4 +1,8 @@ -
+{% load cf_ui %} +
{% for choice in choices %} diff --git a/src/cf_ui/templates/cotton/_themes/bulma/icon.html b/src/cf_ui/templates/cotton/_themes/bulma/icon.html index d5cc94e..96b744c 100644 --- a/src/cf_ui/templates/cotton/_themes/bulma/icon.html +++ b/src/cf_ui/templates/cotton/_themes/bulma/icon.html @@ -1,4 +1,6 @@ +{% load cf_ui %} diff --git a/src/cf_ui/templates/cotton/_themes/daisy/badge.html b/src/cf_ui/templates/cotton/_themes/daisy/badge.html index dec6968..d923867 100644 --- a/src/cf_ui/templates/cotton/_themes/daisy/badge.html +++ b/src/cf_ui/templates/cotton/_themes/daisy/badge.html @@ -1,3 +1,5 @@ +{% load cf_ui %} {{ slot }} diff --git a/src/cf_ui/templates/cotton/_themes/daisy/box.html b/src/cf_ui/templates/cotton/_themes/daisy/box.html index 8ccafff..552e837 100644 --- a/src/cf_ui/templates/cotton/_themes/daisy/box.html +++ b/src/cf_ui/templates/cotton/_themes/daisy/box.html @@ -1,3 +1,4 @@ +{% load cf_ui %} {% comment %}The border colour lives entirely in the variant, `neutral` included. Splitting it — a default colour in the base and an override in the variant — puts two border-color utilities of equal specificity on one element, and which @@ -5,4 +6,5 @@ map.{% endcomment %}
{{ slot }}
diff --git a/src/cf_ui/templates/cotton/_themes/daisy/checkbox-group.html b/src/cf_ui/templates/cotton/_themes/daisy/checkbox-group.html index e9b7a9b..c77c800 100644 --- a/src/cf_ui/templates/cotton/_themes/daisy/checkbox-group.html +++ b/src/cf_ui/templates/cotton/_themes/daisy/checkbox-group.html @@ -1,4 +1,8 @@ -
+{% load cf_ui %} +