{#- chirp-ui: Data Grid Server-driven interactive data grid — the citable, drop-in composite for sortable columns (with aria-sort + server sort state), row selection bound to a selection bar, sticky header + sticky first column, and HTMX load-more. All state is projected from the typed `chirp_ui.grid_state` helper: the macro renders the `aria_sort` and toggle `next_url` it never computes, so the server's ORDER BY and the rendered headers cannot drift. Selection is the only client concern — one idempotent `chirpuiGridSelection` Alpine factory (in chirpui-alpine.js) owns live in-page toggling. The server seeds checked/indeterminate from `SelectionState` so selection is correct with JavaScript off. Works without Chirp (render the macro with precomputed ColumnSort rows), better with Chirp (template globals + use_chirp_ui Alpine injection). Route + template usage: from chirp_ui import Column, parse_sort, sort_columns, selection_state, sort_query COLS = [Column("name","Name",sortable=True), Column("status","Status",sortable=True,align="center"), Column("seats","Seats",sortable=True,align="right")] @app.get("/users") def users(req): offset = int(req.query.get("offset", 0)) sort = parse_sort(req.query.get("sort"), default_key="name", allowed=tuple(c.key for c in COLS)) rows = query_users(order_by=sort.key, desc=(sort.direction=="desc"), offset=offset, limit=PAGE) cols = sort_columns(COLS, sort, base_url="/users", extra_params={"q": req.query.get("q","")}) sel = selection_state(req.query.getlist("ids"), page_ids=[u.id for u in rows], total=count_users()) ctx = dict(columns=cols, rows=[[u.name,u.status,u.seats] for u in rows], row_ids=[u.id for u in rows], row_labels=[u.name for u in rows], selection=sel, selection_id="users", has_more=offset + PAGE < count_users(), load_more_url=f"/users?offset={offset + PAGE}&sort={sort_query(sort)}") # Load-more fetch → bare