# Calendar - agent guide

> Normative extract of the EchoSistema template for the page
> `/componentes/calendario`. 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`.

```vue
<echo-calendar
  v-model:view="view"        <!-- month | week | day | list -->
  v-model:date="focusDay"    <!-- YYYY-MM-DD -->
  :events="visible"
  :week-start="1"
  :max-per-day="3"
  :day-start="7" :day-end="22"
  @create="openDrawer"       <!-- click on empty space -> 'YYYY-MM-DD' -->
  @open="editEvent"          <!-- click on an entry -> EchoCalendarEvent -->
/>

<echo-calendar-mini v-model="focusDay" :events="visible" :week-start="1" />
```

```ts
interface EchoCalendarEvent {
  id: string
  title: string
  start: string      // 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:mm', LOCAL
  end?: string       // last day, inclusive
  allDay?: boolean
  label?: string     // category (the filter's key)
  intent?: EchoIntent
  mode?: 'in-person' | 'video' | 'phone' | 'messenger'
  channel?: string   // 'meet' | 'zoom' | 'teams' | 'whatsapp' | 'telegram' | 'signal' | …
  location?: string  // in person
  contact?: string   // number (phone) or account (messenger)
  url?: string       // link to join (video)
  description?: string
}
```

## How an entry happens

Four modes, because they are four different gestures for whoever is about to
attend: go somewhere, open a link, dial a number, open an app. An absent `mode`
is `in-person`.

| Mode | The field it asks for | Icon |
| --- | --- | --- |
| `in-person` | `location` | pin |
| `video` | `channel` + `url` | camera |
| `phone` | `contact` | phone |
| `messenger` | `channel` + `contact` | speech bubble |

In the form, the mode is the field that decides the next ones: showing all four
at once would be asking for three answers nobody is going to give. On save only
the chosen mode's field travels - a meeting link left over from a call that
became a phone call is a link somebody will click.

## The rest

Auto-imported utilities (`app/utils/calendar.ts`): `isoDay`, `parseDay`,
`startOfDay`, `addDays`, `addMonths`, `isSameDay`, `startOfWeek`, `monthGrid`
(always 42 days), `weekDays`, `eventOnDay`, `eventsOfDay`, `minutesOfDay`. All
in LOCAL time: `toISOString()` reads UTC and would move half the events.

The component does NOT fetch, does NOT filter and does NOT create. Filtering by
category is plain code in the page; "new event" is an `<echo-drawer>` the page
owns - a drawer and not a modal, because whoever is filling in a date wants to
see the week behind it.

Surface classes: `.es-cal__cell`, `.es-cal__daynum`, `.es-cal__event`,
`.es-cal__block`, `.es-cal__row`, `.es-cal__dot`. The event chip is the system's
NOTICE surface (the same recipe as `.es-alert`): a ring in
`color-mix(--i 30%, --border)`, a tint at 8% and a thinner wedge.

---

Source: `app/components/echo/{Calendar,CalendarMini}.vue + app/utils/calendar.ts`
