Skip to content

Add a primitives layer: button, icon, badge, heading, label across all five themes #52

Description

@fsecada01

Summary

cf-ui 0.2.0 ships 14 components across 5 themes, and they are all structural — breadcrumb, card, checkbox-group, form-field, modal, navbar, notification, pagination, panel, progress, select, table, tabs, textarea. There is no primitives layer: no button, icon, badge, heading, or standalone label.

That gap is what stops a consuming repo from going all-in. Measured against RankedJobs (108 templates, ~86 using CSS-framework classes), the five most-used constructs are button (204 occurrences), label (129), title (108), table (90), and icon (83). cf-ui covers exactly one of them. Meanwhile four components it does ship — modal, tabs, panel, breadcrumb — have zero uses in that repo.

So a repo adopting cf-ui today abstracts its rare markup and hand-writes its common markup. That is backwards, and it is why adoption has stalled at 1 component (<c-cf.notification>) after three months.

Why this belongs upstream rather than in each consumer

The house family spans two template engines — Jinja (FastAPI, Litestar) and django-cotton (Django) — and the same primitives are about to be written twice, in resume_maker and RankedJobs, in the same quarter. Cross-engine reuse is the thing cf-ui exists for, and primitives are where the duplication is highest because they are the highest-frequency markup in every repo.

It also matters for atelier, which is a production multi-tenant white-label CMS where the theme layer is a product surface rather than an internal convention. A primitives layer that a non-designer can compose without producing something broken is a feature there, not a refactor.

Proposed primitives

Tiered by measured frequency, so the first tier can ship and be useful before the rest exists.

Tier 1 — the high-frequency five

Component Why Notes
button 204 uses in the reference repo; the single most common construct in every consumer Needs variant (primary/danger/…), size, state (loading/disabled), and a link-vs-button element choice
icon 83 uses The hardest to abstract — see open questions
badge 72 uses (tag/tags) Bulma tag → daisy badge → Bootstrap badge → Fomantic label → Foundation label
heading 123 uses (title/subtitle) Level (h1–h6) decoupled from visual size
label 129 uses Standalone. form-field already bundles a label, but most uses are outside a form field

Tier 2 — containers

Component Why
box / surface 59 uses. A plain bordered/elevated container; distinct from card, which imposes header/body/footer
prose / content 61 uses. Typographic reset for server-rendered or user-supplied HTML

Tier 3 — layout

grid (25 columns + 28 column + 53 width tokens in the reference repo). Deliberately last, and possibly out of scope — see open questions.

Per-component cost

Following the existing layout, one component is 11 files:

  • templates/cotton/cf/<name>.html — engine-neutral base
  • templates/cotton/_themes/{bootstrap,bulma,daisy,fomantic,foundation}/<name>.html — 5
  • templates/jinja/{bootstrap,bulma,daisy,fomantic,foundation}/<Name>.jinja — 5

Tier 1 is therefore 55 files, Tier 2 is 22. Mechanical once the prop contract is settled — the prop contract is the actual design work, and it should be settled for all of Tier 1 at once rather than per component, because these compose with each other (a button contains an icon; a badge sits inside a heading).

Open questions

  1. Icons have no cross-framework answer. Bootstrap ships Bootstrap Icons, Bulma assumes Font Awesome, daisyUI assumes nothing and defers to Tailwind + whatever SVG set you bring. There is no class-level abstraction that works across all five. Options: (a) icon takes a slot and only supplies sizing/alignment, (b) cf-ui adopts one icon set as a peer dependency, (c) icon is dropped from Tier 1. Option (a) is the least opinionated and probably right, but it delivers less than the usage count suggests.

  2. Is grid in scope at all? Bootstrap, Bulma, Foundation, and Fomantic all ship 12-column grid systems with different class vocabularies; daisyUI ships none and defers entirely to Tailwind utilities. So the daisy variant of a grid component would emit raw Tailwind while the other four emit framework classes — a genuine asymmetry rather than a thin adapter. Worth deciding explicitly before building.

  3. Does button need to render both <button> and <a>? Every framework styles them identically, and consumers use both. An href prop that switches the element is the obvious answer, but it interacts with the disabled state (an <a> cannot be disabled) and needs a documented rule.

  4. Escaping. 0.2.0 made Jinja templates escape their own output, so any prop deliberately carrying markup must be markupsafe.Markup. Primitives take more caller-supplied content than the structural components do — prose/content exists precisely to wrap untrusted HTML. The escaping contract for these needs to be explicit in the docs, not inferred.

Acceptance criteria

  • Tier 1 (button, badge, heading, label, and icon subject to Q1) implemented as base + 5 cotton themes + 5 jinja themes each.
  • Prop contracts designed as a set before implementation, and documented — including how they compose (icon inside button, badge inside heading).
  • Every theme variant renders idiomatic markup for its framework, not a lowest-common-denominator div soup.
  • Variant/size/state vocabularies are closed sets validated the same way axis token values are in axes.py — a bad value fails the build, not review.
  • Docs page per component on the docs site, matching the existing component pages.
  • The escaping contract is stated for every prop that can carry markup.
  • Tier 2 and Tier 3 tracked separately so Tier 1 can ship independently.

Notes

Raised from the RankedJobs UI-refresh assessment (Ranked-Jobs#242). That repo is about to migrate ~86 templates from Bulma to Tailwind + daisyUI and would consume this immediately; resume_maker is the other near-term consumer on the Jinja side. Neither should build these locally — the point of the package is that the second consumer gets them free.

Frequency figures are measured from RankedJobs and are illustrative of the class of repo, not a universal ranking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions