Skip to content

feat(primitives): box and prose across five themes (#54) - #59

Merged
fsecada01 merged 4 commits into
masterfrom
feat/component-framework-ui-54-tier-2-primitives
Aug 1, 2026
Merged

feat(primitives): box and prose across five themes (#54)#59
fsecada01 merged 4 commits into
masterfrom
feat/component-framework-ui-54-tier-2-primitives

Conversation

@fsecada01

Copy link
Copy Markdown
Owner

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 from card. Takes variant.

<Cf:Prose> / <c-cf.prose> — a typographic reset for a block of server-rendered or user-supplied markup. Takes size.

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, not surfacebox is Bulma's own class name; surface is Material vocabulary that no shipped framework uses.

prose, not content — rejected twice over. content collides with Bulma's own .content class, which is the exact class this component maps to on that theme, and content is 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 primary and secondary are not colours on a segment. They are its emphasis variation — .ui.primary.segment renders 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 prose has a real gap, not merely an inert axis. It styles bare h1-h5 and p globally, so headings and paragraphs come out right — but it ships no bare ul, ol or table rule 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 in docs/primitives.md as a gap with the workaround that does work (<c-cf.table> inside the block, ui list on 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 prose deliberately does nothing

On Bootstrap and Foundation it emits no class at all, and that is the correct answer rather than a gap: both style bare h1-h6, p and ul globally, so the reset this component exists to scope is already in effect everywhere.

daisy and @tailwindcss/typography (acceptance criterion 4)

prose comes from the @tailwindcss/typography plugin — not from daisyUI, not from Tailwind core. It is now a declared requirement of that one component on that one theme, documented in docs/daisyui.md with 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 principle AXIS_KINDS encodes.

Escaping (acceptance criterion 5)

prose is 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 for Markup / mark_safe to 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.md names nh3 and bleach.

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 in CLASSES proves those themes agree — so prose rendering 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.

IMPLEMENTED was a hand-written tuple. Adding a primitive to PRIMITIVES and themes.COMPONENTS without 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

  • box and prose as base + 5 cotton themes + 5 jinja themes each — 22 files.
  • Names chosen and justified.
  • No new axis was needed; both reuse variant / size, so AXIS_KINDS, VOCABULARIES and CLASS_VALUED are untouched and the existing parity and drift tests picked both components up without modification.
  • The daisy prose decision documented in docs/primitives.md and docs/daisyui.md.
  • The escaping contract stated explicitly in docs/escaping.md.
  • Docs coverage matching the Tier 1 components, plus the stale ## Not yet implemented section 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 check clean · ruff format --check clean · 65 JS tests pass · mkdocs build --strict clean · prek run --all-files all 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

fsecada01 and others added 3 commits July 31, 2026 21:06
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
@fsecada01 fsecada01 added enhancement New feature or request documentation Improvements or additions to documentation labels Aug 1, 2026
@fsecada01 fsecada01 self-assigned this Aug 1, 2026
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
@fsecada01
fsecada01 merged commit d140713 into master Aug 1, 2026
7 checks passed
@fsecada01
fsecada01 deleted the feat/component-framework-ui-54-tier-2-primitives branch August 1, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Primitives Tier 2: box/surface and prose/content across all five themes

1 participant