Skip to content

feat(themes): DaisyUI theme + django-cotton theme dispatch (#6) - #9

Merged
fsecada01 merged 3 commits into
masterfrom
feat/component-framework-ui-phase-6-daisyui-theme
Jul 29, 2026
Merged

feat(themes): DaisyUI theme + django-cotton theme dispatch (#6)#9
fsecada01 merged 3 commits into
masterfrom
feat/component-framework-ui-phase-6-daisyui-theme

Conversation

@fsecada01

Copy link
Copy Markdown
Owner

Closes #6.

Implements the DaisyUI theme in both template sets, plus the dispatch mechanism django-cotton needed before a theme switch was possible at all.

Acceptance criteria

  • 14 components in both template sets, passing the existing three-tier suite (unit, integration, E2E Playwright with js_on and js_off).
    jinja/daisy/*.jinja (14) and cotton/_themes/daisy/*.html (14). 393 unit+integration, 42 E2E (2 skipped — the existing js_off-only guards).
  • Switching from Bulma to DaisyUI is a config change, with no template edits in the consuming app.
    CF_UI_THEME = "daisy" on Django, theme="daisy" on FastAPI/Litestar. The DaisyUI E2E server renders the same consumer templates as the Bulma one — tests/integration/cotton_app/templates/ is not duplicated or edited — so this is executed, not asserted.
  • Documented Tailwind content-glob, verified by a test that would fail if the templates were tree-shaken.
    docs/daisyui.md + cf_ui.themes.tailwind_content_globs(); tests/unit/test_tailwind_content.py covers both ways tree-shaking actually happens.
  • Alpine behaviors (cfModal, cfNavbar, cfPanel, cfTabs) work unchanged — theme-independent and should stay that way.
    cf_ui_alpine.js is untouched. Only the toggled class differs (is-activemodal-open / tab-active), and E2E exercises open/close, dismiss, expand, burger and tab activation under DaisyUI.

The one design decision worth reviewing

The issue says to fill templates/cotton/daisy/. That directory layout no longer exists — 0.1.1 removed per-theme cotton directories and CF_UI_THEME did not select a cotton path at all, so the cotton set had no way to switch themes. I raised this before implementing; the chosen approach was partial dispatch:

{# cotton/cf/card.html — the public component, path unchanged #}
<c-vars header="" footer="" class="" />
{% load cf_ui %}{% cf_ui_theme_path "card" as cf_ui_partial %}{% include cf_ui_partial %}

Theme markup moves to cotton/_themes/<theme>/<name>.html. {% include %} inherits the caller's context, so props and {{ slot }} reach the partial without redeclaring <c-vars> — it stays in exactly one place per component.

Why not the obvious alternative of setting COTTON_DIR per theme: that is precisely the 0.1.1 regression (#4). COTTON_DIR is a single global prefix for every <c-foo.bar> lookup, so setting it breaks any consumer whose own components live at cotton/<their-app>/*.html. tests/unit/test_consumer_compatibility.py still passes unchanged.

Consequence worth a second opinion: each wrapper now does {% load cf_ui %}, so "libraries": {"cf_ui": "cf_ui.templatetags.cf_ui"} is required for components to render, not just for the asset tags. It was already documented as required (the cf_ui.django app label defeats templatetag autodiscovery), but the failure mode for anyone who skipped it moves from "asset tags missing" to "nothing renders." Called out explicitly in docs/daisyui.md and the README.

Tailwind tree-shaking

This is the failure mode the issue flagged, and it is silent — correct markup, no styling, no error. Two distinct causes, both tested:

  1. The content glob misses site-packages. tailwind_content_globs() returns absolute globs derived from cf_ui.__file__, so they work from an editable or site-packages install; python -m cf_ui.themes prints them. The docs are checked against the same constant, so they cannot drift.
  2. Class names assembled at render time. Tailwind's scanner reads source text, so alert-{{ type }} is invisible to it. Every variant class is written out in full instead, and a test scans the DaisyUI templates for class tokens split across a template construct. The detector has a meta-test, since a guard that cannot fail is not a guard.

The prop vocabulary is deliberately unchanged across themes: type="danger" still works; DaisyUI has no alert-danger, so the templates map it onto alert-error internally.

Not in the issue, but blocking

pyproject.toml gained [tool.ruff.lint.isort] known-first-party = ["cf_ui"] (separate commit). The pinned ruff-pre-commit v0.3.0 and the ruff CI resolves from dev (0.16.x) classify cf_ui differently under a src/ layout and each reverted the other's import fix — prek green, CI red on the file the hook had just fixed. Stating it explicitly makes both agree. This bit issue #5 too.

Follow-ups not folded in

  • cotton/{bootstrap,daisy,fomantic,foundation} and jinja/{bootstrap,fomantic,foundation} still hold PLANNED.md stubs; the daisy ones are now gone. Migrating the remaining three to _themes/ is their own tickets.
  • Review findings still open from Theme composition axes — pluggable token layer #5 (unvalidated axis token values, --spacing aliased for all themes, p3 outside the contrast gate) are untouched here.

Verification

pytest tests/unit tests/integration   393 passed
pytest tests/e2e --browser chromium    42 passed, 2 skipped
ruff check src tests                   All checks passed
ruff format --check src tests          58 files already formatted
prek run --all-files                   all hooks passed

fsecada01 and others added 2 commits July 29, 2026 00:13
Adds the full 14-component DaisyUI set to both template sets and the
mechanism django-cotton needed to switch themes at all.

The two engines dispatch differently, deliberately. JinjaX already
switched by directory. django-cotton could not: <c-cf.card> resolves
through a single global COTTON_DIR, which cf-ui must not touch (0.1.1
reverted exactly that, because it broke consumers whose own components
live at cotton/<their-app>/*.html). So the public component stays at the
fixed path cotton/cf/<name>.html and becomes a wrapper that declares
<c-vars> and includes cotton/_themes/<theme>/<name>.html. Consumer
templates are untouched — the E2E tier proves it by rendering the Bulma
gallery's own templates under CF_UI_THEME="daisy".

Tailwind tree-shaking is the failure mode this theme actually risks, and
it fails silently. Both causes are tested: cf_ui.themes publishes the
absolute content globs (docs are checked against the same constant), and
every variant class is written out in full rather than assembled as
alert-{{ type }}, which the scanner cannot see.

Alpine is unchanged — cfModal, cfNavbar, cfPanel, cfTabs and the $cf
store are theme-independent; only the toggled class differs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
The pinned ruff-pre-commit hook (v0.3.0) and the ruff CI resolves from
`dev` (0.16.x) infer first-party differently for a src/ layout, so each
reverted the other's import fix: prek went green and CI then failed I001
on the file the hook had just "fixed". Stating known-first-party removes
the ambiguity for both versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
@fsecada01 fsecada01 added the enhancement New feature or request label Jul 29, 2026
Two defects found by adversarial review of the PR diff, both of which the
original tests were too weak to catch.

tailwind_content_globs() emitted native Windows paths. Those strings are
pasted into a tailwind.config.js or an @source directive, where a
backslash is a JavaScript escape and fast-glob reads it as an escape
rather than a separator — so the glob matches nothing and every class is
shaken out, which is the exact silent failure the API exists to prevent.
The old test only asserted Path.is_absolute(), which backslashes satisfy.

The navbar toggled `flex` onto an element that kept `hidden`. Both are
display utilities in the same layer and Tailwind emits `hidden` last, so
the class list changed and the menu still never appeared. The E2E test
asserted classList membership, so it passed a broken component; it now
asserts computed display, verified to fail against the old markup.

The DaisyUI E2E gallery now loads the Tailwind play CDN. DaisyUI ships
component classes but no utilities, so without it `hidden` and `lg:flex`
resolved to nothing and this tier could not observe layout behavior at
all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
@fsecada01

Copy link
Copy Markdown
Owner Author

Adversarial review — PR #9

Run against the PR diff (74 files, +2203/−333). Findings were produced by executing probes against the built code, not by reading it. Two real defects found and fixed in 853d98c; both had passed the original tests, which is the more useful part of the result.


1. tailwind_content_globs() emitted unusable paths on Windows — FIXED

'C:\\dev\\...\\cf_ui\\templates\\**\\*.html'

These strings are pasted into a tailwind.config.js or an @source directive. In JS a backslash is an escape character, and fast-glob (Tailwind's matcher) treats \ as an escape rather than a separator on every platform. The documented glob would therefore have matched nothing — shaking out every class and producing correct markup with no styling and no error.

That is precisely the silent failure this API exists to prevent, so shipping it here would have been the worst possible place for it.

Why the tests missed it: test_content_globs_resolve_to_absolute_paths asserted Path(pattern).is_absolute(), which a backslash path satisfies, and test_content_globs_cover_every_daisy_template used Python's glob, which handles backslashes fine on Windows. Both green, both blind to the actual consumer.

Fix: .as_posix(), plus test_content_globs_use_forward_slashes_on_every_platform.

2. DaisyUI navbar burger changed classes but never showed the menu — FIXED

The menu was class="navbar-end hidden lg:flex" with :class="{ 'flex': menuOpen }". hidden and flex are both display utilities in the same layer, and Tailwind emits hidden after flex, so display: none won regardless of the toggle. Clicking the burger changed the class list and nothing else.

Fix: toggle hidden off instead — class="navbar-center lg:flex" with :class="{ 'hidden': !menuOpen }". This is the canonical hidden lg:flex idiom and relies on media-query ordering rather than intra-layer emission order, so it is deterministic in every state.

Why the test missed it: the E2E assertion checked classList.contains('flex'), which was true. It now asserts getComputedStyle(el).display, and I verified the new test fails against the old markup before restoring the fix:

AssertionError: assert 'none' != 'none'

3. The DaisyUI E2E tier was not observing layout at all — FIXED

The gallery linked DaisyUI's CDN CSS, which ships component classes but no Tailwind utilities. So hidden, lg:flex, w-full and friends resolved to nothing, and no computed-style assertion about them could have been meaningful. Finding #2 was only observable after adding the Tailwind play CDN to the DaisyUI gallery head.


Verified, not assumed

Probes executed against the running code:

  • Context bleed through {% include %}. The dispatch inherits the caller's context, so a page variable could in principle be read by a partial. Rendered cotton/cf/card.html with label and items deliberately set in the page context — neither leaked into the output. <c-vars> defaults shadow them.
  • Nested dispatch. Rendered a card inside an outer template that had itself set cf_ui_partial. {% include %} pushes a context layer, so the inner wrapper's assignment does not clobber the outer one — confirmed restored after the include returns.
  • Runtime theme flips. Alternating CF_UI_THEME between renders in one process produces the right markup each time; nothing is cached at import.
  • COTTON_DIR regression (fix: stop hijacking COTTON_DIR — cotton/cf/ template layout #4) has not returned. test_consumer_compatibility.py passes unchanged, and the new code touches no template settings.
  • Real Cotton compilation. Unit tests use render_to_string, which bypasses the Cotton compiler — the dispatch is only proven by E2E, which passes. <c-vars> is consumed by the compiler and the named header slot survives the {% include %}.

Open, not blocking

  • {% load cf_ui %} in every wrapper makes the libraries setting load-bearing for rendering, not just for the asset tags. It was already documented as required, but a consumer who skipped it now gets "nothing renders" instead of "asset tags missing." Called out in docs/daisyui.md and the README. Alternatives (a context processor, or builtins) need the same consumer config; injecting it from ready() is what fix: stop hijacking COTTON_DIR — cotton/cf/ template layout #4 forbids.
  • The DaisyUI modal has no role="dialog" / aria-modal. Pre-existing — the Bulma modal does not either — so not a regression from this PR, but worth its own ticket.
  • Tabs show no active tab without JS, since tab-active lives in an Alpine binding. Exact parity with Bulma's is-active; noting it as a shared limitation rather than a new one.
  • cotton/{bootstrap,fomantic,foundation} and jinja/{bootstrap,fomantic,foundation} still hold PLANNED.md stubs. Migrating them to _themes/ belongs to their own tickets.
  • Review findings still open from Theme composition axes — pluggable token layer #5 (unvalidated axis token values → injection, --spacing aliased for all themes, p3 outside the contrast gate) are untouched here.

Gates

pytest tests/unit tests/integration   394 passed
pytest tests/e2e --browser chromium    42 passed, 2 skipped
ruff check src tests                   All checks passed
ruff format --check src tests          58 files already formatted
prek run --all-files                   all hooks passed

Note that prek passing is not by itself evidence here: the pinned hook (ruff v0.3.0) and CI's ruff (0.16.x) were actively reverting each other's import fixes on this branch. That is fixed at the root in bbd0ea2 by declaring known-first-party = ["cf_ui"], and both now agree and are idempotent.

@fsecada01 fsecada01 self-assigned this Jul 29, 2026
@fsecada01
fsecada01 merged commit c9036f8 into master Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DaisyUI theme — implement component set

1 participant