{# Form field macros - renders the appropriate input for each field type #} {# Contract: ~/.claude/skills/ux-architect/components/form-field.md (UX-017) #} {# Core branches: pure Tailwind + design-system variables. #} {# Widget branches (combobox, multi_select, tags, picker, range, color, #} {# rich_text, slider, money, file, search-select) retain their own markup #} {# and are out of scope for UX-017 — tracked by UX-009..015. #} {% 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.hint is defined and field.hint else "" %} {% set described_by = ([error_id] if error else []) + ([hint_id] if hint_id else []) %} {% set base_input = "w-full h-8 px-3 rounded-[4px] bg-[hsl(var(--background))] border text-[13px] text-[hsl(var(--foreground))] placeholder:text-[hsl(var(--muted-foreground))] focus:outline-none focus:ring-1 focus:ring-[hsl(var(--ring))] transition-[box-shadow,border-color] duration-[120ms] [transition-timing-function:cubic-bezier(0.2,0,0,1)]" %} {% set border_idle = "border-[hsl(var(--border))]" %} {% set border_error = "border-[hsl(var(--destructive))]" %}
{% if field.type == "checkbox" %} {# Checkbox: label wraps input + text (inline row) #} {% elif field.source %} {# UX-028: search-select dynamic search with autofill #} {% if field.hint is defined and field.hint %}

{{ field.hint }}

{% endif %} {% include "fragments/search_select.html" %} {% if error %} {% endif %} {% elif field.ref_entity %} {# Entity-ref auto-wiring (cycle 236 — closes EX-044). When a DSL field is a plain `ref Entity` with no explicit `source:` override, the template compiler populates ref_entity + ref_api from the IR and the form renders a select that hydrates options from the entity's list endpoint. Mirrors the Alpine fetch pattern already used by filter_bar.html for ref/belongs_to column filters. #} {% if field.hint is defined and field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %} {# ── Vendored widget overrides (Phase 4) ─────────────────────────── #} {% elif field.widget == "combobox" %} {# UX-009: combobox widget — TomSelect wrapper, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "multi_select" %} {# UX-021: multiselect widget — TomSelect with remove_button plugin, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "tags" %} {# UX-022: tags widget — TomSelect with create:true, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "picker" and field.type in ("date", "datetime") %} {# UX-010: datepicker widget (single) — Flatpickr wrapper, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "range" and field.type in ("date", "datetime") %} {# UX-010: datepicker widget (range) — Flatpickr wrapper, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %} {% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "color" %} {# UX-024: colorpicker widget — Pickr wrapper, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %}
{% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "rich_text" %} {# UX-025: richtext widget — Quill editor wrapper, pure Tailwind chrome #}
{% if field.hint %}

{{ field.hint }}

{% endif %}
{% if errors.get(field.name) %} {% endif %}
{% elif field.widget == "slider" %} {# UX-023: slider widget — native + dzRangeTooltip controller #}
{% if field.hint %}

{{ field.hint }}

{% endif %}
{% if errors.get(field.name) %} {% endif %}
{% else %} {# Standard fields: label above #} {% if field.hint is defined and field.hint %}

{{ field.hint }}

{% endif %} {% if field.type == "textarea" %} {% elif field.type == "select" %} {% elif field.type == "date" %} {% elif field.type == "datetime" %} {% elif field.type == "money" %} {# UX-026: money widget — dzMoney Alpine, pure Tailwind chrome #} {# Major-unit display input + hidden minor-unit value carrier #} {% set minor_val = values.get(field.name ~ '_minor', '') if values else '' %} {% if field.extra.get('currency_fixed', true) %} {# Pinned currency — static prefix symbol #}
{% else %} {# Unpinned currency — dropdown selector #}
{% endif %} {% elif field.type == "number" %} {% elif field.type == "email" %} {% elif field.type == "file" %} {# UX-027: file upload — Alpine dzFileUpload, pure Tailwind chrome #}
{# File preview (shown when file exists) #}
{# Dropzone (shown when no file) #} {# Upload progress #}
{# Client-side error message (file-too-large, etc.) — distinct from server errors #}
{% else %} {# Default: text input #} {% endif %} {% endif %} {% if error %} {% endif %}
{% endmacro %}