{# The components that float above the page. Basecoat's own JavaScript drives all four, and it is already vendored and mounted: it scans for `.popover`, `.dropdown-menu`, `.select` and `.combobox` on load and again on `htmx:afterSwap`, so a swapped-in overlay works with nothing extra. No macro below registers a handler. What they share is id wiring: a trigger and its panel find each other through four related ids, and one wrong id fails silently — the panel opens but is never announced, or never opens at all. Every macro here takes one `id` and derives the rest, which is why they are macros and not snippets to copy. `drawer` is the exception: a native `` that needs no JS from Basecoat, only `showModal()`. `multiselect_scripts()` is the one macro here that adds a file to the page, and only a page using `multiple=true` calls it. It fixes the wire format rather than the widget — see its own comment. #} {% from "ui/attrs.html" import attrs %} {% from "ui/icon.html" import icon %} {# _message(field_id, hint, error) — the paragraph under a scripted control. A second copy of the macro `ui/form.html` calls under every field, written out here rather than imported. Jinja refuses to import a name beginning with an underscore, and renaming that one to make it importable would publish a helper no app template should call. It would also change what `fjkit eject` does with it: a private helper is copied into the file you own, while a public one is re-exported and keeps moving under you. `_GAP` is written out twice for the same reason. It differs from that copy in one way, and the difference is the point: it always renders the paragraph, empty and hidden when there is nothing to say. `js/errors.js` reuses the `

` a control names in `aria-describedby` and creates one where it finds none, inserting it directly after the control. For the two macros below, the control carrying `name` is a hidden input at the end of a wrapper div, so "after" is inside that wrapper and the 422 would draw its red text inside the select box. Reserving the paragraph gives it a target in the right place. #} {% macro _message(field_id, hint=none, error=none) -%} {%- if error %}

{{ error }}

{%- elif hint %}

{{ hint }}

{%- else %} {%- endif -%} {%- endmacro %} {# Panel widths. A popover has to be told how wide to be — its content is out of flow, so it has no column to fill — and upstream does that with a utility at the call site. A closed lookup instead, for the usual two reasons: an app template may not write `w-72` (A2), and an interpolated width renders while missing from the stylesheet. #} {% set _PANEL_WIDTHS = { "auto": "", "sm": "w-40", "default": "w-56", "lg": "w-72", "xl": "w-96", } %} {# popover(id, label, …) — a panel of arbitrary content, anchored to a button. Content arrives through `{% call %}`. The trigger is a `button` macro's worth of parameters rather than a second slot, because a popover whose trigger is not a button is nearly always a `dropdown_menu` or a `tooltip`. `aria-expanded` starts false and Basecoat's JS flips it; `aria-hidden` on the panel works the same way. Both are written here rather than left off: first paint is the state before any JS has run, and a trigger with no `aria-expanded` is announced as a plain button. #} {% macro popover(id, label, variant="outline", size="", side="bottom", align="center", width="lg", icon_name=none) -%}
{%- endmacro %} {# dropdown_menu(id, label, …) — a menu of commands. The panel holds a `role="menu"` labelled by the trigger, and the items are `menu_item` / `menu_group` / `menu_separator` below. Three ids have to line up for that: trigger, popover and menu. All derived. A menu is not a ` {%- else %} {%- endif %} {%- endmacro %} {# select_menu(name, options, …) — the scripted listbox. Prefer `select_field`. A native `` renders as a scrolling box nobody has ever found by accident. The value reaches the server through the hidden input, which carries `name`; the visible trigger is a button and submits nothing. That is why `name` is the first parameter and the button has none: a form that posts the button posts nothing at all. {{ select_menu("labels", options=label_options, selected=task.labels, multiple=true) }} `label` is an `aria-label` on the trigger and draws nothing. Pass `visible_label` where the control sits in a form beside `text_field` and has to look like one: it renders the `.field` wrapper, a real `