{% from "ui/attrs.html" import attrs %} {% from "ui/icon.html" import icon %} {# badge(label, variant) — variant: primary|secondary|outline|destructive|success|warning|info #} {% macro badge(label, variant="") -%} {{ label }} {%- endmacro %} {# card(title, description, size, actions, padded) — a bordered panel, called as a block macro. {% call card("Throughput", "last 7 days") %}

…

{% endcall %} `actions` takes pre-rendered markup for the right-hand side of the header: {% set filters %}{% call button_group() %}…{% endcall %}{% endset %} {% call card("5 tasks", actions=filters) %}…{% endcall %} A parameter rather than a second slot: a two-slot call block runs its body once per slot, and the common case is a heavy body with no actions at all. `padded=false` drops the inner
so a table sits flush against the card's edges. With a header as well, the body gets a rule above it; a flush body butting into header text reads as one block. #} {% macro card(title=none, description=none, size="", actions=none, padded=true) -%} {%- set has_header = title or description or actions -%}
{%- if has_header %}
{% if title %}

{{ title }}

{% endif %} {% if description %}

{{ description }}

{% endif %}
{% if actions %}{{ actions }}{% endif %}
{%- endif %} {% if padded %}
{{ caller() }}
{% elif has_header %}
{{ caller() }}
{% else %} {{ caller() }} {% endif %}
{%- endmacro %} {# Never build a class by interpolation (`text-{{ tone }}`): Tailwind finds classes by scanning source text, so an interpolated one is absent from the stylesheet. A closed lookup keeps every value in this file verbatim, where the scan finds it. #} {% set _TONES = { "success": "text-success", "warning": "text-warning", "info": "text-info", "destructive": "text-destructive", "muted": "text-muted-foreground", } %} {# stat(label, value, hint, icon_name, tone) — the KPI tile. #} {% macro stat(label, value, hint=none, icon_name=none, tone=none) -%}

{{ label }}

{{ value }}

{% if hint %}

{{ hint }}

{% endif %}
{% if icon_name %} {{ icon(icon_name, 18) }} {% endif %}
{%- endmacro %} {# metric_group(items) — a compact row of counts, the small sibling of stat(). `items` is a list of (label, value) pairs. Use it inside a card when the numbers are context for something else; use stat() when they are the point of the page. #} {% macro metric_group(items, cols=3) -%} {%- set _GRID = {2: "grid-cols-2", 3: "grid-cols-3", 4: "grid-cols-4"} -%}
{% for label, value in items %}
{{ label }}
{{ value }}
{% endfor %}
{%- endmacro %} {# progress(value, label) — value is 0-100. #} {# `id` makes the bar addressable, which is what `form(progress=…)` needs: an upload's percentage arrives in the browser, so something has to write it into markup the server rendered once at zero. The three hooks below are that contract. A bar has three places the same number is written — the width, `aria-valuenow`, and the printed percentage — and moving one without the others is worse than moving none: a bar that fills while the label still reads 0%, or fills for a sighted reader while a screen reader is still told nothing has happened. They are `data-*` rather than more ids because there is one of each per bar, and an id per part would have to be derived from `id` and kept in step at both ends. #} {% macro progress(value, label=none, id=none) -%}
{% if label %}
{{ label }}{{ value }}%
{% endif %}
{%- endmacro %} {# empty_state(title, description, icon_name) — the zero-row case, once. #} {% macro empty_state(title, description=none, icon_name="sparkle") -%}
{{ icon(icon_name, 22) }}

{{ title }}

{% if description %}

{{ description }}

{% endif %}
{%- endmacro %} {# bullet_list() / list_item() — prose lists inside a card. {% call bullet_list() %} {% call list_item() %}Templates are compiled once per process.{% endcall %} {% endcall %} #} {% macro bullet_list(tone="muted") -%} {%- endmacro %} {% macro list_item() -%}
  • {{ caller() }}
  • {%- endmacro %} {# caption(text) — a standalone line of secondary prose. `card`, `section` and `page_header` each render a description, but each of those is the second line of a title block and cannot move away from its heading. This is the line that stands alone: under a table, beneath a control, at the foot of a form. An app template cannot write one by hand — the colour it needs (`text-muted-foreground`) is a utility the checker rejects, and a bare `

    ` renders at body weight. Takes the text as an argument, or a body when the caption holds a `link()`: {{ caption("Counts refresh every five minutes.") }} {% call caption() %} Source: {{ link("the trial registry", href=registry_url) }} {% endcall %} #} {% macro caption(text=none) -%}

    {{- caller() if caller is defined else text -}}

    {%- endmacro %} {# link(label, href) — an inline link in body copy. Always underlined: a link distinguished by colour alone fails for a reader who cannot see the colour. #} {% macro link(label, href) -%} {{ label }} {%- endmacro %} {# kbd(keys) — a keyboard key or a literal token in running text. #} {% macro kbd(keys) -%} {{ keys }} {%- endmacro %} {# code_block(source, label, wrap) — source, a log, a config, a payload. Roadmap 0.9, pulled forward. Basecoat has no component for this and `card` does not reach it: a code region has three problems a bare
     leaves open.
    
      1. It scrolls, so it has to be reachable. A mouse can wheel a scroll
         container that nothing can focus; a keyboard cannot reach it at all.
         `tabindex="0"` fixes that, and it is why this clears CHARTER §8's bar on
         wrapping a native element.
      2. A focusable region needs a name, or a screen reader announces an
         unlabelled stop in the tab order. `role="region"` is applied only when
         there is a label for it, because an unnamed region is worse than none.
      3. Long lines. Pass `wrap=true` for emitted markup: one line of HTML holding
         an inline SVG is not something to scroll sideways through.
    
      The content is printed as text and stays text. Highlighting is client-side —
      a page that wants it reads `textContent` back and marks up the tokens — which
      keeps the source legible with scripting off and keeps this macro from having
      to know any language.
    #}
    {% macro code_block(source, label=none, wrap=false) -%}
      
    {{ source }}
    {%- endmacro %} {# item_list() / item(title, description, …) — a list of rows that are not a table: settings, results, definitions, a feed. Roadmap 0.9. Basecoat ships `.item-group` / `.item`, and its markup contract is load-bearing: `.item > section` is the text column, `.item > figure` the leading icon, `.item > aside` the trailing actions. An app should not have to re-derive that from a stylesheet. {% call item_list() %} {{ item("router.py", "reads the request, calls the service") }} {{ item("service.py", "the actual work", icon_name="gauge") }} {% endcall %} `description` may be markup, so a row can hold a badge or a link without the caller reaching for |safe. Basecoat clamps `.item p` to two lines, which suits a feed and not a definition list; `clamp=false` releases it. It is an attribute rather than a class because the rule lives in `fjkit.css`, beside the other `data-*` extensions of Basecoat's API. #} {% macro item_list() -%}
    {{ caller() }}
    {%- endmacro %} {% macro item(title, description=none, icon_name=none, actions=none, href=none, clamp=true) -%} {%- set tag = "a" if href else "div" -%} <{{ tag }} class="item"{% if not clamp %} data-clamp="false"{% endif %} {%- if href %} href="{{ href }}"{% endif %} {%- if kwargs %}{{ attrs(kwargs) }}{% endif %}> {%- if icon_name %}
    {{ icon(icon_name, 16) }}
    {% endif %}

    {{ title }}

    {%- if description %}

    {{ description }}

    {% endif %}
    {%- if actions %}{% endif %} {%- endmacro %} {# description_list(layout) / description_item(term, value) — term-and-value pairs. A native `
    `, so a screen reader announces each value as the definition of the term beside it. `item_list` covers most of what this is used for and does not carry that relationship: it is a list of things, and this is a list of facts about one thing. Every pair is wrapped in a `
    ` — permitted inside `
    ` since HTML 5.2, and required here, because a grid over bare `
    `/`
    ` children lays them out in document order and cannot keep a pair on one row. {% call description_list() %} {{ description_item("Status", badge("Active", "success")) }} {{ description_item("Owner", task.owner) }} {% endcall %} `value` may be markup, like `item`'s `description`, so a row can hold a badge or a link without the caller reaching for `|safe`. Where the value is longer than one expression, `{% call description_item("Notes") %}…{% endcall %}` puts it in the body instead; `value` wins if both are given. Two layouts, and the difference is only where the term sits. `rows` moves it beside the value once the viewport is wide enough for two columns. `stacked` keeps it above at every width, which is what a card too narrow for a label column needs. Neither is a class the caller writes — see `fjkit.css`, where the `
    ` margin reset that both depend on also lives. #} {% set _DL_LAYOUTS = ("rows", "stacked") %} {% macro description_list(layout="rows") -%}
    {{ caller() }}
    {%- endmacro %} {% macro description_item(term, value=none) -%}
    {{ term }}
    {{ value if value is not none else caller() }}
    {%- endmacro %} {# avatar(name, src, size, badge_tone, badge_icon) `name` is required and `src` is not: the initials are what the image falls back to, and an avatar with no name has nothing to show when the URL 404s. Basecoat stacks both and lets the absolutely positioned `` cover the initials once it loads, so the fallback costs nothing and needs no JavaScript. `name` also becomes the `alt` text, so the accessible name and the visible initials cannot disagree. The badge is a status dot. Upstream's example writes `bg-green-600` on it; here the colour is a role (A3), because a dot meaning "online" has to survive a rebrand exactly like a badge meaning "done". #} {% set _AVATAR_BADGE_TONES = { "success": "bg-success", "warning": "bg-warning", "info": "bg-info", "destructive": "bg-destructive", "muted": "bg-muted-foreground", } %} {% macro avatar(name, src=none, size="", initials=none, badge_tone=none, badge_icon=none) -%} {# Two letters at most: "Ada Lovelace" is AL, "Ada" is A, and a five-word name is not five letters. Pass `initials` where the default does not survive — a handle, a non-Latin script. #} {%- set letters = initials if initials is not none else (name.split() | map("first") | join)[:2] | upper -%} {%- if src %}{{ name }}{% endif %} {# The initials carry the accessible name when there is no image, and are hidden from the tree when there is: otherwise the avatar is announced twice, once as the alt text and once as two stray letters. #} {%- if badge_tone or badge_icon %} {%- if badge_icon %}{{ icon(badge_icon, 12) }}{% endif -%} {%- endif %} {%- endmacro %} {# avatar_group(overflow) Overlapping avatars, with an optional "+3" at the end. `overflow` is a number rather than a rendered label so the macro writes the "+" itself: a caller passing "3 more" breaks the ring geometry the CSS gives `[data-count]`. #} {% macro avatar_group(overflow=none, label=none) -%}
    {{- caller() -}} {%- if overflow %}+{{ overflow }}{% endif -%}
    {%- endmacro %}