feat(primitives): box and prose across five themes (#54) - #59
Merged
fsecada01 merged 4 commits intoAug 1, 2026
Conversation
Tier 2 of the primitives layer. `<Cf:Box>` / `<c-cf.box>` is a plain bordered
container — one element, no imposed header/body/footer, which is what
separates it from `card`. `<Cf:Prose>` / `<c-cf.prose>` is a typographic reset
for a block of server-rendered or user-supplied markup. `box` takes `variant`,
`prose` takes `size`; a container's size is its content's business and a
typographic reset has no colour, so neither takes more.
Named `box` and `prose`, not `surface` and `content`. `box` is Bulma's own
class name and `surface` is Material vocabulary no shipped framework uses.
`content` was rejected twice over: it collides with Bulma's own `.content`
class, and `content` is already the prop every Jinja primitive uses for slot
text.
Two per-framework facts were verified against upstream docs rather than
assumed, and both would have shipped wrong:
Fomantic's `primary` and `secondary` are not colours on a segment — they are
its emphasis variation, so `.ui.primary.segment` renders a subdued treatment
rather than a brand fill. The variant maps to real hues instead. The one theme
whose vocabulary appears to match cf-ui's by name is the one theme where
matching it would be wrong.
Fomantic also styles bare h1-h5 and p globally but ships no bare ul, ol or
table rule at all, so lists and tables inside a prose block fall back to
browser defaults and no scoping class fixes it. Documented as a gap with the
workaround that does work, rather than papered over with a cf-ui-authored type
scale. On Bootstrap and Foundation `prose` emits nothing, and that is correct
rather than missing — both style typography globally already. On daisy it
requires @tailwindcss/typography, now a declared dependency of that one
component on that one theme.
Fixes two guards that could not fail:
The theme-dispatch test compared raw rendered strings and required all five
themes to differ. A {% comment %} block leaves its own blank lines behind, so
two partials emitting byte-identical markup landed in different buckets on
newline count alone — gutting a partial's entire class chain left it green.
It now compares collapsed markup and allows a collision only when the class
map proves the themes agree.
`IMPLEMENTED` was a hand-written tuple, so adding a primitive without editing
that one line shipped its templates with zero parity and guard coverage,
silently. It is derived now, which inverts the failure mode.
docs/escaping.md states the sanitization contract outright for the first
time: cf-ui does not sanitize caller-supplied HTML.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
An isolated subagent run creates a checkout under `.claude/worktrees/`, and `git add -A` swept one in as an embedded repository — which clones silently without its contents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
`prek run --all-files` restyles all 12 of #54's new box/prose templates, which means every contributor's hook run would dirty files they did not touch. Committing djlint's own output makes the reformat a no-op. Note this is *not* a lint failure — `just lint-templates` reports 0 errors on both trees before and after. djlint's check mode does not enforce `single_attribute_per_line`; only `--reformat` applies it, so the drift was invisible to the gate. Full suite still 2273 passed / 13 skipped: the change is layout inside the opening tag only, and no `class="…"` value moved, which is the line that matters here — added whitespace inside a class value changes the rendered bytes and breaks the substring assertions the primitives parity tests rely on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
5 tasks
Picks up #56 (the daisy CDN fix) and #57 (the gitignore chore). One conflict, in CHANGELOG.md, and it was purely additive: both sides appended a new section under [Unreleased] and neither contradicts the other. Both kept, newest first — #54's Added/Fixed pair above #56's. Renamed master's bare `### Fixed` to `### Fixed — daisyUI's CDN recipe was missing its utility layer (#56)`. Every other heading in the file carries the em-dash summary, and after this merge there are two `Fixed` sections side by side, so a bare one is ambiguous as well as inconsistent. Content is verbatim from master. Gate on the merged tree: 2311 passed, 13 skipped · ruff check and ruff format --check clean on src tests · djlint 0 errors on both template trees · mkdocs build --strict clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #54.
Tier 2 of the primitives layer: two container primitives, all five themes, both template sets. 22 new templates plus the class maps, docs and tests.
The two components
<Cf:Box>/<c-cf.box>— a plain bordered container. One element, no imposed header/body/footer, which is exactly what separates it fromcard. Takesvariant.<Cf:Prose>/<c-cf.prose>— a typographic reset for a block of server-rendered or user-supplied markup. Takessize.Neither takes more than one axis, on purpose: a container's size is its content's business, and a typographic reset has no colour — giving it one would mean colouring every nested element.
Naming (acceptance criterion 2)
box, notsurface—boxis Bulma's own class name;surfaceis Material vocabulary that no shipped framework uses.prose, notcontent— rejected twice over.contentcollides with Bulma's own.contentclass, which is the exact class this component maps to on that theme, andcontentis already the prop name every Jinja primitive uses for slot text.Two framework facts that were verified, and would have shipped wrong
I checked the per-theme class claims against upstream documentation instead of asserting them. Two of ten came back refuted.
Fomantic's
primaryandsecondaryare not colours on a segment. They are its emphasis variation —.ui.primary.segmentrenders a subdued treatment, not a brand fill. So the variant maps to real hues (blue,grey,green,yellow,red,teal) instead. The one theme whose vocabulary appears to match cf-ui's by name is the one theme where matching it would have been wrong, and the failure would have been visual-only.Fomantic's
prosehas a real gap, not merely an inert axis. It styles bareh1-h5andpglobally, so headings and paragraphs come out right — but it ships no bareul,olortablerule at all. That styling lives on.ui.list/.ui.table, on the element itself. So a list or table inside a prose block falls back to browser defaults, and no scoping class fixes it. Documented indocs/primitives.mdas a gap with the workaround that does work (<c-cf.table>inside the block,ui liston lists), rather than papered over by cf-ui authoring its own Fomantic type scale — that would mean shipping component CSS this package has never shipped and guessing at a framework's scale.Where
prosedeliberately does nothingOn Bootstrap and Foundation it emits no class at all, and that is the correct answer rather than a gap: both style bare
h1-h6,pandulglobally, so the reset this component exists to scope is already in effect everywhere.daisy and
@tailwindcss/typography(acceptance criterion 4)prosecomes from the@tailwindcss/typographyplugin — not from daisyUI, not from Tailwind core. It is now a declared requirement of that one component on that one theme, documented indocs/daisyui.mdwith the install and config lines. Without the plugin the class is unrecognised and the block renders unstyled, which is the benign class-valued failure mode rather than broken markup — the same principleAXIS_KINDSencodes.Escaping (acceptance criterion 5)
proseis the first primitive whose entire purpose is wrapping caller-supplied markup. Mechanically nothing changed: it is still slot-based, and cf-ui's own output is still{% autoescape true %}-wrapped. What had never been written down anywhere, and now is, is that cf-ui does not sanitize. A caller reaching forMarkup/mark_safeto get real markup into a prose block has taken that on, and if the HTML came from a user it must be sanitized first —docs/escaping.mdnamesnh3andbleach.Two guards that could not fail
Both found by mutation, not by reading.
The theme-dispatch test was passing on whitespace. It compared raw rendered strings across the five themes and required all five to differ. A
{% comment %}block leaves its own blank lines behind, so two partials emitting byte-identical markup still landed in different buckets on newline count alone. Proven: gutting a partial's entire class chain left the test green. It now compares collapsed markup, and permits a collision only when the component's entry inCLASSESproves those themes agree — soproserendering alike on three themes passes for a stated, derived reason, while a partial that silently drops an axis still fails. Re-verified by mutation in both directions.IMPLEMENTEDwas a hand-written tuple. Adding a primitive toPRIMITIVESandthemes.COMPONENTSwithout also editing that one line shipped its templates with zero parity and guard coverage, and nothing failed to say so. It is now derived from the intersection of the two, which inverts the failure mode: a primitive registered without templates fails loudly instead of quietly going unchecked. This is the trap that would have hit this very PR.Acceptance criteria
boxandproseas base + 5 cotton themes + 5 jinja themes each — 22 files.variant/size, soAXIS_KINDS,VOCABULARIESandCLASS_VALUEDare untouched and the existing parity and drift tests picked both components up without modification.prosedecision documented indocs/primitives.mdanddocs/daisyui.md.docs/escaping.md.## Not yet implementedsection corrected — it still claimed badge/heading/label/icon had no templates, false since feat(primitives): a primitives layer — button, badge, heading, label, icon (#52) #53.Gate
2298 passed, 13 skipped (up from 2145) ·
ruff checkclean ·ruff format --checkclean · 65 JS tests pass ·mkdocs build --strictclean ·prek run --all-filesall 10 hooks pass.Related: #55 (Tier 3,
grid— a decision rather than a build) and #58 (the daisy CDN bug, independent).🤖 Generated with Claude Code
https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf