{# Form field macros — render the appropriate input for each field type. Contract: ~/.claude/skills/ux-architect/components/form-field.md (UX-017) v0.62 CSS refactor (complete): every branch (search-select, ref-entity, combobox, multi_select, tags, picker, range, color, rich_text, slider, plus the standard else-block) now emits semantic classes from `components/form.css`. The `aria-invalid="true"` attribute drives the destructive-border state via `.dz-form-input[aria-invalid="true"]` in CSS — no more conditional border-class concatenation per branch. #} {% macro render_field(field, values={}, errors={}) %} {% set value = values.get(field.name, field.default) if values else field.default %} {% set error = errors.get(field.name, "") if errors else "" %} {% set field_id = "field-" ~ field.name %} {% set error_id = "error-" ~ field.name %} {% set hint_id = "hint-" ~ field.name if field.help is defined and field.help else "" %} {% set described_by = ([error_id] if error else []) + ([hint_id] if hint_id else []) %}
{{ field.help }}
{% endif %} {% elif field.source %} {# UX-028: search-select dynamic search with autofill #} {% if field.help is defined and field.help %}{{ field.help }}
{% endif %} {% include "fragments/search_select.html" %} {% if error %}{{ error }}
{% endif %} {% elif field.ref_entity %} {# Entity-ref fields render as TomSelect comboboxes with remote load (#939). The framework previously had two paths here — one Alpine + x-for that populated an empty select, one TomSelect that loaded via its own callback. The Alpine path was the implicit default and the TomSelect path opt-in via widget=combobox; both fetched the same FK list endpoint with different lifecycles, contending over the same DOM node when both fired (the wrapper-on-wrapper smell from #927). The single path now: empty