{# The data table: structure, sortable headers, batch selection and the pagination that goes under it. {% call table( columns=[{"label": "Task", "sort": "asc", "sort_url": "/tasks?o=-title"}, {"label": "Owner"}, {"width": "min"}], rows=tasks, target="#board", empty_title="Nothing here", empty_description="No task matches this filter.") %} {% for task in tasks %}{{ task_row(request, task) }}{% endfor %} {% endcall %} {{ pagination(page, pages, url_for(request, "tasks_page"), target="#board") }} Passing `rows` gives the macro the empty case, so no caller repeats the "if there are rows … else …" branch. Pass `rows=none` to always render the body yourself. Rows are a macro call in the caller's loop, never `{% include %}`: include costs a template lookup plus a fresh context object per iteration, and the gap widens linearly with row count. #} {% from "ui/attrs.html" import attrs %} {% from "ui/button.html" import button %} {% from "ui/data.html" import empty_state %} {% from "ui/icon.html" import icon %} {% set _ALIGN = {"start": "text-left", "center": "text-center", "end": "text-right"} %} {% set _WIDTH = {"min": "w-px", "narrow": "w-16", "auto": ""} %} {# `aria-sort` is what a screen reader reads out of the header; the glyph is what everyone else reads. Both are keyed off one value, so they cannot disagree — an arrow pointing up over `aria-sort="descending"` is the usual way this component goes wrong. A third state is in neither table: sortable, but not the column currently sorted. It falls through to `aria-sort="none"` and the two-headed chevron. Without that chevron a sortable column looks exactly like a fixed one. #} {% set _ARIA_SORT = {"asc": "ascending", "desc": "descending"} %} {% set _SORT_ICON = {"asc": "arrow-up", "desc": "arrow-down"} %} {# table(columns, rows, …) — the