# Forms - agent guide

> Normative extract of the EchoSistema template for the page
> `/componentes/formularios`. Copy it verbatim: tag names, props, events and classes
> are contract.
>
> Full guide (rules, tokens and every family): `/agents/echosistema-ui.md`

## Rules that apply before any line of code

- Colour only through tokens (`text-fg1`, `bg-panel`, `border-line`, `.es-i--*`); never a hex, never a Tailwind colour.
- Two axes: the intent gives the colour, the variant consumes it. Never combine them by hand.
- Visible text through `t()`; tag names, props and classes stay literal.
- A component does not fetch: it takes props and emits events.
- An icon alone inside a button needs an `aria-label`.

Whole compositions. The rules below are the normative part of this page.

1. **Columns on purpose.** Two columns only for short fields that belong
   together. Everything half-width reads as a table; nothing half-width wastes a
   third of the screen on a twenty-character name.
2. **Mark what is required** - and if nearly everything is required, invert it
   and mark what is optional.
3. **Validate on submit.** Flagging a field on every keystroke is the commonest
   way a form turns hostile.
4. **One filled action**, at the end and aligned to the end.
5. **Group with a legend:** each block is a `<fieldset>` with its `<legend>`.
6. **The right `autocomplete`** on every field.

## The skeleton of a form

```vue
<form novalidate @submit.prevent="send()">
  <fieldset class="min-w-0 border-0 p-0">
    <legend class="es-tag mb-3 p-0">{{ t('forms.address.who') }}</legend>

    <div class="grid gap-4 sm:grid-cols-2">
      <echo-field v-slot="{ id, fieldName }" :label="t('forms.fields.name')" name="name" required>
        <echo-input :id="id" v-model="form.name" :name="fieldName" type="text" autocomplete="name" />
      </echo-field>
      …
    </div>
  </fieldset>

  <div class="mt-6 flex flex-wrap items-center justify-end gap-2.5">
    <echo-button type="reset">{{ t('forms.actions.clear') }}</echo-button>
    <echo-button type="submit" variant="filled" intent="primary">{{ t('forms.actions.send') }}</echo-button>
  </div>
</form>
```

## The filter bar

A filter has to become a URL, not component state: a listing filtered in memory
alone cannot be shared or reloaded.

```vue
<form class="grid gap-4 lg:grid-cols-[minmax(0,2fr)_repeat(3,minmax(0,1fr))_auto] lg:items-end"
      @submit.prevent="router.push({ query })">
```

---

Source: `app/pages/componentes/formularios.vue`
