{# 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 %} {# 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 %}