# 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`) | **Permanent** |

**Not every pattern component is present in every app.** `AppLayout`, `AppHeader`,
`AppFooter`, `AppSideMenu`, and `SessionExpiredBanner` are base app-shell — `vue-app`
provisions and wires all five into `App.vue` for every generated app, unconditionally.
The rest are on-demand: they only exist in a given app's `src/lib/patterns/` if that
app has run the matching view generator at least once.

| Component | Provisioned by |
|---|---|
| `RecordDetailShell` | `vue-detail-view` |
| `WorkspaceTable` | `vue-workspace-view`, `vue-admin-crud` |
| `Stepper`, `StepErrorSummary` | `vue-intake-view` |

Don't assume one of these on-demand components exists in a given app — check whether
its generator has actually been run (e.g. `nx list @abgov/nx-adsp` won't tell you;
look for the component file itself, or the view file that would import it) before
building on 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.

## 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 |

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 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-')`).
