# AGENTS.md — vue-components (GoA Design System Vue wrappers + app shell)

A shared, workspace-local library — every Vue app in this workspace imports from
it instead of carrying its own copy. Generated by `@abgov/nx-adsp:vue-components`,
invoked automatically by `vue-app` and by every `vue-*-view` generator.

| Folder | Contains | Lifespan |
|---|---|---|
| `src/lib/primitives/` | Thin `v-model`/idiomatic-event wrappers over individual `goa-*` elements (`GoabInput`, `GoabButton`, …) | **Interim** — see below |
| `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`, `Stepper`, `StepErrorSummary`, `FilterBar`) | **Permanent** |
| `src/lib/formatters.ts` | Shared value formatting for views (`formatDate`, `formatDateTime`, `formatNumber`, `formatPercent`, `optionLabel`, `badgeType`) | **Permanent** |

**Every pattern component listed above is present in every app.** This lib is generated
in one shot, so all of `primitives/` and all of `patterns/` exist as soon as any Vue app
has been generated — there is no per-component provisioning step and nothing to check for
before building on one.

Which components are *wired up* does vary: `vue-app` mounts the shell components
(`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`) in
`App.vue`, while the rest are imported by whichever view generator uses them —
`RecordDetailShell` by `vue-detail-view`, `WorkspaceTable` by `vue-workspace-view` and
`vue-admin-crud`, `Stepper`/`StepErrorSummary` by `vue-intake-view`, `FilterBar` by a
filterable workspace view. An unused component is idle, not missing: import it directly
in a hand-authored view rather than running a generator to "provision" it.

> **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
> only because GoA DS has not yet published an official Vue wrapper package. When
> `@abgov/vue-components` ships, delete `primitives/` and repoint imports at it —
> the component names and props are deliberately kept matching, so it should be a
> scope swap, not a rewrite. Don't add features here that would make that swap
> harder (see "Don't" below).
>
> **`patterns/` is not part of that swap.** App-shell composition (layout,
> header, footer, banners) has no equivalent in an official design-system
> package — a design system ships primitives, not your app's shell. Nothing in
> `patterns/` is deleted when `primitives/` is.

## Use the design system's own elements — most need no wrapper

**Read this before writing any markup.** GoA's design system ships ~94 custom elements. Only a
handful need a Vue wrapper (the form inputs — see the next section for why); the rest are
presentational, have nothing to bind, and are used **bare** in any Vue SFC. `vite.config.mts`'s
`isCustomElement` already recognises `goa-*`, so they work with no import and no registration.

If you find yourself writing `style="…"` for layout, spacing, or typography, stop — the element you
want almost certainly exists. Measured on a real ADSP app that shipped without this catalog: 254 of
its 257 inline styles were on raw HTML (`div`, `th`, `td`, `span`, `h2`) standing in for the elements
below, while only 3 were on `goa-*` elements. Every view its generators fully covered had zero
inline styles.

| Instead of hand-rolling | Use | Notes |
|---|---|---|
| `<div style="background:white;border:1px solid …;border-radius:…;padding:…">` | `<goa-container>` | The bordered, padded card box. `type`/`accent` variants |
| `<div style="display:flex;gap:…;align-items:…">` | `<goa-block>` | Flex row or stack, `gap` and `direction` props |
| `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(…))">` | `<goa-grid>` | Responsive auto-fit grid, `minchildwidth` prop (all lowercase — `min-child-width` silently does nothing) |
| `<h2 style="font-size:…;font-weight:…;color:…">`, styled `span`/`p` | `<goa-text>` | Typography scale: `size`, `weight`, `color`, `mt`/`mb` |
| `<table>` with `<th style>`/`<td style>` | `<goa-table>` (+ `<goa-table-sort-header>`) | For any table that isn't a paginated list view — `WorkspaceTable` already wraps this for that case |
| Tab `<button>`s toggling `v-if` blocks | `<goa-tabs>` + `<goa-tab heading="…">` | Slot-based and self-managing: `initialtab` sets the first open tab, no `v-model` and no `@_change` |
| A margin-only `<div>` or `<br>` | `<goa-spacer vspacing="m">` | |
| A styled status pill | `<goa-badge type="…">` | |
| A hand-built alert/notice box | `<goa-callout type="…" heading="…">` | |
| A loading `<div>` or spinner markup | `<goa-skeleton type="…">` or `<goa-spinner>` | Skeletons match the shape they replace (`card`, `text`, `table`) |
| A `<label>` + error `<span>` around an input | `<goa-form-item label="…" error="…" mb="l">` | Wrap the `Goab*` input in this, don't restyle it. `mb` defaults to none, so stacked items sit flush against each other — pass `mb="l"` (the design system's own convention for vertical forms). Omit it when the items are inside a `goa-block`, which supplies its own gap |
| A row of buttons with flex styling | `<goa-button-group alignment="…">` | |
| A hand-built multi-select (checkbox list, chips) | `<goa-dropdown-multiselect>` | Added in web-components 2.4.0 |
| A hand-built filter row above a table | `<FilterBar>` (this library) | Controls, collapse, active-filter chips and clear-all. It emits a query-ready values object; the *view* owns resetting the page, debouncing, and URL sync |

Also available bare and worth knowing before hand-rolling: `goa-accordion`, `goa-details`,
`goa-divider`, `goa-chip`, `goa-filter-chip`, `goa-icon`, `goa-icon-button`, `goa-link`,
`goa-notification`, `goa-popover`, `goa-tooltip`, `goa-file-upload-input`,
`goa-file-upload-card`, `goa-circular-progress`, `goa-linear-progress`, `goa-hero-banner`,
`goa-page-block`. For the full set, list the registered element names from the installed package:

```bash
grep -rhoE 'customElements\.define\("goa-[a-z0-9-]+"' node_modules/@abgov/web-components \
  | grep -oE 'goa-[a-z0-9-]+' | sort -u
```

For values rather than layout, use `formatters.ts` (`formatDate`, `formatDateTime`, `formatNumber`,
`formatPercent`) rather than an inline `toLocaleString` — every view should render a date or a count
the same way, and each formatter already returns an em dash for a null/unparseable value so a
partially-populated record needs no per-field guard.

### Never render a stored code to a person

Two more formatters exist because rendering a *coded* value is where generated views have most often
been wrong, and the failure is invisible to a unit test — it only shows up when someone reads the
screen.

| Don't | Do |
| --- | --- |
| `{{ record['species'] }}` → shows `cattle-mature` | `{{ optionLabel(fieldOptions['species'], record['species']) }}` → `Cattle (mature)` |
| `<goa-badge type="information" :content="row['status']" />` — every status the same neutral blue | `<goa-badge :type="badgeType(columnBadgeTypes['status'], row['status'])" :content="optionLabel(...)" />` |

`optionLabel` falls back to the raw value for an unmapped code (a data or config problem you want to
see, not hide); `badgeType` falls back to `information`, which is the honest colour for a value whose
severity nobody declared.

The view generators build these maps for you — but only from what you pass them. Supply `options` on
any `--fields`/`--columns` entry whose stored value is a code, and `badgeMap` on a `badge` entry, e.g.
`badgeMap: { approved: 'success', declined: 'emergency' }`. Omit them and the view renders the code.
A badge's colour is the fastest thing a reader takes in, so a declined claim in neutral blue reads as
routine — this matters most on a check-your-answers page, whose entire purpose is that the person can
read back what they entered.

## What these wrappers do (and why they're needed)

`goa-*` are framework-agnostic custom elements (built with Svelte). Three quirks
make them awkward in Vue, and each wrapper exists to smooth exactly one of them:

- **No `v-model`.** The elements emit a **`_`-prefixed** custom event (`_change`,
  `_click`, `_close` — the prefix avoids colliding with native DOM events), *not*
  the `input`/`change` events Vue's `v-model` listens for. So a plain
  `<goa-input>` needs manual `:value` + `@_change` wiring every time.
- **The new value arrives on `event.detail`, not `event.target.value`.** You read
  `$event.detail.value` (or `.checked` for a checkbox) — reading `.target.value`
  silently gives you nothing useful.
- **Bindings are properties, not attributes.** Bind `:value` / `:checked` /
  `:open` and Vue sets them as element properties, which is what these components
  read.

Each wrapper adds real `v-model` (via `defineModel`) over exactly **one** `goa-*`
element and lets every other prop/event fall through (`inheritAttrs`), so anything
the design system supports keeps working without being re-declared.

## Event contract (read the value off the GoA event)

| Wrapper | Underlying element | Model | Read from event |
|---|---|---|---|
| `GoabInput`, `GoabTextarea`, `GoabDropdown`, `GoabRadioGroup` | `goa-input` / `-textarea` / `-dropdown` / `-radio-group` | `string` | `$event.detail.value` |
| `GoabCheckbox` | `goa-checkbox` | `boolean` | `$event.detail.checked` |
| `GoabButton` | `goa-button` | — (re-exposes `_click` as `@click`) | — |
| `GoabModal` | `goa-modal` | `open: boolean` (`v-model:open`) | `_close` clears it |
| `GoabDatePicker` | `goa-date-picker` | `Date \| undefined` | `$event.detail.value` is a **`Date`**, not a string (`valueStr` carries the string form) |

Most value elements put the value on `detail.value`; some carry extra keys
(dropdown adds `label`, radio-group adds `labels`) you can ignore unless you need
them. Always confirm against the component's page — see below.

## Wrapping a new component (`primitives/`)

### 1. Find the event contract

The source of truth is the component's page at
[design.alberta.ca/components](https://design.alberta.ca/components/) — check its
**Events** and **Properties**. If the detail shape isn't spelled out there,
confirm it against the shipped source (the elements dispatch a `CustomEvent` whose
`detail` object names its keys):

```bash
# event names an element dispatches (all are _-prefixed):
grep -oE '"_[a-zA-Z]+"' node_modules/@abgov/web-components/index.js | sort -u

# the detail keys for a change (e.g. an input dispatches { name, value };
# a checkbox { name, checked, value }) — search near the element's dispatch:
grep -oE 'detail:[^}]*\}' node_modules/@abgov/web-components/index.js | head
```

### 2. Pick the pattern by the element's shape

| Element shape | Model | Skeleton |
|---|---|---|
| Text-like value (input, textarea, dropdown, date, …) | `string` | **A** |
| Boolean toggle (checkbox, switch) | `boolean` | **B** |
| Action only, no value (button) | — | **C** |
| Open/close overlay (modal, drawer) | `boolean` via `v-model:open` | **D** |
| Container with `*-item` children (dropdown, radio-group) | `string` | **A** + pass items through `<slot />` |

### 3. Copy the matching skeleton

Keep the `INTERIM WRAPPER` header comment (see the existing files), swap the
element name and the `detail` key, then `export` it from `src/index.ts`.

**A — value component**
```vue
<script setup lang="ts">
const model = defineModel<string>();
function onChange(e: Event) {
  model.value = (e as CustomEvent<{ value: string }>).detail.value;
}
</script>
<template>
  <goa-thing :value="model" @_change="onChange"><slot /></goa-thing>
</template>
```

**B — boolean component** (bind `:checked`, read `detail.checked`)
```vue
<script setup lang="ts">
const model = defineModel<boolean>();
function onChange(e: Event) {
  model.value = (e as CustomEvent<{ checked: boolean }>).detail.checked;
}
</script>
<template>
  <goa-thing :checked="model" @_change="onChange"><slot /></goa-thing>
</template>
```

**C — action only** (no model; re-expose the `_`-event as a plain one)
```vue
<script setup lang="ts">
const emit = defineEmits<{ click: [event: CustomEvent] }>();
</script>
<template>
  <goa-thing @_click="emit('click', $event as CustomEvent)"><slot /></goa-thing>
</template>
```

**D — open/close overlay** (named model so it's `v-model:open`)
```vue
<script setup lang="ts">
const open = defineModel<boolean>('open');
</script>
<template>
  <goa-thing :open="open" @_close="open = false"><slot /></goa-thing>
</template>
```

Don't re-declare props you don't transform — let them fall through. `name` in
particular is required by most `goa-*` form elements (it's echoed in the event
detail); just leave it to fall through from the caller.

## Building a pattern component (`patterns/`)

A pattern component is app-shell composition — layout, header/footer chrome,
banners — not a single-element wrapper. Existing examples: `AppLayout`,
`AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`,
`Stepper`, `StepErrorSummary`.

- It's fine to compose `primitives/` wrappers inside a pattern component (e.g.
  `SessionExpiredBanner` uses `GoabButton`) — import them with a relative path
  (`../primitives/GoabButton.vue`), not the package's own public import path.
- Stay presentational: accept data and behavior via props/`v-model`/`emit`, don't
  fetch data, call routing APIs, or encode business rules. A pattern component
  used by every app in the workspace can't assume any one app's routes or
  domain — that belongs in the app that uses it (see `SessionExpiredBanner`'s own
  comment for a live example: the component renders the banner, but which
  Keycloak hook flips `show` is main.ts's job, in the consuming app).
- `export` it from `src/index.ts` under the "patterns" group, with a one-line
  purpose comment in this file's app-shell components table (mirrored in each
  consuming generator's own `AGENTS.md`, e.g. `vue-app/files/AGENTS.md__tmpl__`).

## Don't

- **Don't write inline `style` for layout, spacing, or typography** in a component here or in a
  view that consumes it. Use the elements in the catalog above. An inline style is a signal either
  that the right element wasn't found, or that a pattern component is missing — both worth raising
  rather than working around.
- **Don't wrap a presentational element.** `primitives/` exists only to add `v-model` over elements
  whose `_`-prefixed event Vue can't bind. `goa-container`, `goa-block`, `goa-grid`, `goa-text`,
  `goa-table`, and `goa-tabs` have no model to bind — a wrapper would add a layer for nothing.
- **Don't add new `goa-*` element wrappers to `patterns/`, or new composite
  components to `primitives/`.** Single-element wrappers belong in
  `primitives/` (interim, deleted on the `@abgov/vue-components` swap);
  composites belong in `patterns/` (permanent). Putting one in the wrong folder
  either blocks that swap or gets deleted by it.
- **Don't add app-specific logic** anywhere in this library (fetching, routing,
  business rules) — it's shared across every app in the workspace. Keep every
  component, wrapper or pattern, presentational.

## Testing

Wrappers are compile-checked and covered by the generator's tests. If you add a
component test that mounts a wrapper, ensure the vite/vitest config treats `goa-*`
as custom elements (`isCustomElement: (tag) => tag.startsWith('goa-')`).
