{# Layout primitives. These exist to keep one promise: an app using fjkit never writes a raw utility class. Components alone do not reach it — two cards side by side means `grid gap-4`, and the closed vocabulary is broken on page one. So arrangement is a component too. Every spacing and column value is a closed lookup whose entries appear verbatim below. Never build a class by interpolation (`gap-{{ n }}`): Tailwind finds classes by scanning source text, so an interpolated one is absent from the stylesheet. An unknown key falls back to the default rather than emitting a class that does not exist. #} {% from "ui/attrs.html" import attrs %} {% set _GAP = { 0: "gap-0", 1: "gap-1", 2: "gap-2", 3: "gap-3", 4: "gap-4", 5: "gap-5", 6: "gap-6", 8: "gap-8", } %} {% set _ALIGN = { "start": "items-start", "center": "items-center", "end": "items-end", "stretch": "items-stretch", "baseline": "items-baseline", } %} {% set _JUSTIFY = { "start": "justify-start", "center": "justify-center", "end": "justify-end", "between": "justify-between", "around": "justify-around", } %} {# stack(gap, align) — vertical flow. {% call stack(4) %} {{ card_a }} {{ card_b }} {% endcall %} flex + gap rather than `space-y-*`: gap cannot collapse, cannot double up when a child is conditionally absent, and reads the same as row(). #} {% macro stack(gap=4, align=none) -%}
{{- caller() -}}
{%- endmacro %} {# row(gap, align, justify, wrap) — horizontal flow. `wrap` defaults to true because a row of buttons or badges that cannot wrap is a horizontal-scrollbar bug waiting for a narrow screen. #} {% macro row(gap=3, align="center", justify=none, wrap=true) -%}
{{- caller() -}}
{%- endmacro %} {# grid(cols, gap) — an even grid that collapses on small screens. `cols` is the wide-screen count; the breakpoints below are fixed. Letting callers choose breakpoints turns a layout vocabulary back into Tailwind with extra steps. #} {% set _COLS = { 2: "sm:grid-cols-2", 3: "sm:grid-cols-2 lg:grid-cols-3", 4: "sm:grid-cols-2 lg:grid-cols-4", } %} {% macro grid(cols=3, gap=4) -%}
{{- caller() -}}
{%- endmacro %} {# split(aside, gap) — main column plus a fixed-width sidebar, stacking on narrow screens. Two slots, so it is called with a slot argument: {% call(slot) split() %} {% if slot == "main" %} …the board… {% else %} …the stats panel… {% endif %} {% endcall %} The body runs once per slot, and each branch renders in exactly one of them. #} {% set _ASIDE = { "sm": "lg:grid-cols-[1fr_16rem]", "md": "lg:grid-cols-[1fr_19rem]", "lg": "lg:grid-cols-[1fr_22rem]", } %} {% macro split(aside="md", gap=6) -%}
{{ caller("main") }}
{%- endmacro %} {# centered(width, gap) — one column, capped and centred. {% call centered("sm") %} {{ brand(…) }} {{ page_header("Sign in", "…") }} {% call card() %}…{% endcall %} {% endcall %} Until this macro there was no way to cap a width. `stack(align="center")` centres a column without capping it, `grid` divides a width it is given, and `split`'s two numbers are grid tracks belonging to the aside. So a sign-in card, a settings form or a page of prose — anything that should not run to the 1152px the shell's wrapper does — left an app two moves: `max-w-sm`, which `fjkit check` rejects as a utility, or an inline `style="width: …"`, which it does not reject and should. Reported by an app that wrote the second one and said so in a comment. A flex column rather than a bare `
`, because every caller wanted a `stack` inside it. `gap` is the closed lookup `stack` uses. `width` is a closed enumeration, for the reason every lookup in this file is: Tailwind finds classes by scanning source text, so `max-w-{{ width }}` names a class absent from the stylesheet, and the column renders at full width with nothing to say it went wrong. The six below cover the cases; a seventh is a one-line change here, not a caller's business. It centres nothing vertically. A sign-in page that wants its card in the middle of the viewport asks the shell for that: leave `header` and `footer_wrapper` empty and the main element is the page. Merging the two jobs would give this macro a parameter that does nothing in the common case. #} {% set _WIDTH = { "xs": "max-w-xs", "sm": "max-w-sm", "md": "max-w-md", "lg": "max-w-lg", "xl": "max-w-xl", "prose": "max-w-prose", } %} {% macro centered(width="sm", gap=6) -%}
{{- caller() -}}
{%- endmacro %} {# fill(justify, align, gap) — take the height the shell already gave `main`, and place the children in it. {% call fill() %} {% call centered("sm") %} {{ page_header("Sign in") }} {% call card() %}…{% endcall %} {% endcall %} {% endcall %} The other direction of `centered`, and deliberately a second macro rather than a parameter on that one: capping a width and filling a height are wanted separately far more often than together, and the sign-in page that wants both says so by nesting them, in the order they read. It grows, rather than asking for a height. `flex-1` inside the shell's `main`, which is a flex column for this reason and no other. The obvious spelling does not work, and the way it fails is silent. `main` takes its own height from `flex-1`, so that height is a used value, not a specified one, and a child asking for `min-height: 100%` resolves it against an indefinite size and gets nothing. The box then fits its content and sits at the top of the page — identical to not calling this macro at all, with no error and no dead class for `fjkit check` to catch. It was written that way first and only a screenshot found it. A viewport unit is the other wrong answer: `min-h-screen` under a header adds the header's height to a box that starts below it, and the page scrolls by exactly that much on the one page nobody expects to scroll. Growing along `main` needs no such arithmetic, and stays right whether the app filled the header block or left it empty for a standalone page. Content taller than the space is not clipped. A flex item's automatic minimum size keeps the box at its content height, and the shell's wrapper is a minimum height rather than a fixed one, so the page grows and scrolls. What this costs: `fill` is for a direct child of `main`. In a block parent it is inert, because `flex-1` describes an item and a block parent has no items to lay out. Vertical centring is what `justify` does here, because the children are a column — the same lookups `row` uses, reading against the other axis. `align` is the cross axis, so it is horizontal, and it is `none` by default: a `centered` inside this one does that job better, with a width to show for it. An unrecognised `justify` falls back to `justify-center` rather than to nothing, which is the one place this file's fallback rule needs saying out loud. Everywhere else an unknown key drops an optional class and the layout is merely plainer. Here the class is the entire macro, and dropping it would leave a box that fills the height and puts its content back at the top — the behaviour of not calling it at all. No gallery entry on the docs site. The preview cells there are sized by their contents, and a macro whose whole subject is the height it was handed has nothing to show in a box that has no height — it would demonstrate the opposite of what it does. The cheatsheet row and `errors/page.html` are where it is documented; CHARTER §5 allows the exemption for exactly this reason. #} {% macro fill(justify="center", align=none, gap=6) -%}
{{- caller() -}}
{%- endmacro %} {# page_header(title, description) — the title block every page opens with. Works plain, or as a call block whose body becomes the right-hand actions: {% call page_header("Tasks", "One board, no page reloads") %} {{ button("New", variant="primary") }} {% endcall %} #} {% macro page_header(title, description=none) -%}

{{ title }}

{% if description %}

{{ description }}

{% endif %}
{% if caller is defined %}{{ caller() }}{% endif %}
{%- endmacro %} {# section(title, description) — a labelled band within a page. Use when a page has more than one topic; a page with one topic just needs page_header. #} {% macro section(title=none, description=none, gap=4) -%}
{% if title or description %}
{% if title %}

{{ title }}

{% endif %} {% if description %}

{{ description }}

{% endif %}
{% endif %} {{- caller() -}}
{%- endmacro %} {# divider() — a horizontal rule that uses the border token, not a hue. #} {% macro divider() -%}
{%- endmacro %}