{# Form controls. Every field takes the same four presentation parameters — `label`, `hint`, `error`, `id` — and wires them the same way: the control gets an id, the label points at it, and `hint`/`error` render into one `
` the control names in `aria-describedby`. An `error` replaces the hint rather than stacking on it: two messages under one control is two things to read when only one of them matters. A rejected submit fills the same `
` from the browser — `js/errors.js` reads FastAPI's 422 and writes each message under the control its `loc` names. Pass `error=` for a message known at first paint. #} {% from "ui/attrs.html" import attrs %} {% from "ui/button.html" import button %} {# form(action, method, target, swap) — the
{%- endmacro %} {# form_scripts() — the script a page loads to be allowed `encoding="json"`. {% block scripts %}{{ form_scripts() }}{% endblock %} Per page, never from the shell. CHARTER §7 budgets what a page downloads by default and the answer has to stay "htmx and Basecoat", so the 1,012 bytes only some forms need are opted into by the page that has one — the way `chart_scripts()` opts into Plotly. `defer` keeps it after htmx: deferred scripts run in document order, htmx is deferred in the shell's head, and the extension calls `htmx.defineExtension` as soon as it runs. An ordinary ` {%- endmacro %} {# field_row(template) — fields side by side on wide screens, stacked on narrow. `template` is a closed lookup key, not a raw grid-template string: an interpolated `sm:grid-cols-[…]` would not exist in the stylesheet. #} {% set _ROWS = { "wide-then-actions": "sm:grid-cols-[1fr_8.5rem_8.5rem_auto]", "two": "sm:grid-cols-2", "three": "sm:grid-cols-3", "four": "sm:grid-cols-2 lg:grid-cols-4", "field-and-button": "sm:grid-cols-[1fr_auto]", } %} {% macro field_row(template="two", gap=3) -%} {%- set _GAP = {2: "gap-2", 3: "gap-3", 4: "gap-4"} -%}