---
phase: 10-ux-interaction
plan: 04
type: execute
wave: 0
depends_on: []
files_modified:
- frontend/src/stores/toast.js
- frontend/src/stores/__tests__/toast.test.js
- frontend/src/components/ui/ToastContainer.vue
- frontend/src/components/ui/__tests__/ToastContainer.test.js
- frontend/src/App.vue
autonomous: true
requirements: [UX-10]
must_haves:
truths:
- "Calling useToastStore().show('msg', 'success', 4000) appends a toast and auto-dismisses after the duration"
- "Calling dismiss(id) removes that toast from the array immediately"
- "Existing Phase 8 call sites (SettingsAccountTab, TotpEnrollment) work unchanged with the locked signature show(message, type, duration)"
- "ToastContainer renders one DOM node per toast, teleported to body, with bottom-right positioning"
- "ToastContainer is mounted at App.vue and visible across all routes (including admin layout)"
artifacts:
- path: "frontend/src/stores/toast.js"
provides: "Reactive toast store with toasts array + show + dismiss"
contains: "toasts"
- path: "frontend/src/components/ui/ToastContainer.vue"
provides: "Visual renderer for the toast stack"
- path: "frontend/src/stores/__tests__/toast.test.js"
provides: "Toast store tests (timers + signature)"
- path: "frontend/src/components/ui/__tests__/ToastContainer.test.js"
provides: "ToastContainer rendering tests"
key_links:
- from: "App.vue"
to: "ToastContainer.vue"
via: " mounted alongside "
pattern: "
Implement the toast notification system (per D-01..D-04): reactive store, visual container, and mount in App.vue. This replaces the Phase 8 no-op stub WITHOUT changing the locked `show(message, type, duration)` signature so that Phase 8 call sites (`SettingsAccountTab.vue`, `TotpEnrollment.vue`) continue to work.
Purpose: Wire UX-10 (toast notification system). Phase 8 only stubbed the store; Phase 10 makes toasts actually visible. Toasts appear bottom-right, stack upward (D-01), use colored left-border accent + inline icon per type (D-02), are teleported to body (D-03).
Output:
- `stores/toast.js` — reactive `toasts` array, `show` action (auto-dismiss via setTimeout), `dismiss` action
- `components/ui/ToastContainer.vue` — Teleport-to-body container with TransitionGroup
- `App.vue` — mount ``
- Vitest tests for both store and container
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@CLAUDE.md
@.planning/phases/10-ux-interaction/10-CONTEXT.md
@.planning/phases/10-ux-interaction/10-RESEARCH.md
@.planning/phases/10-ux-interaction/10-PATTERNS.md
@frontend/src/stores/toast.js
@frontend/src/App.vue
@frontend/src/components/ui/SearchableModelSelect.vue
@frontend/src/components/settings/SettingsAccountTab.vue
@frontend/src/components/auth/TotpEnrollment.vue
**Toast store (setup-store form — D-04 LOCKED signature):**
```js
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useToastStore = defineStore('toast', () => {
const toasts = ref([]) // [{ id, message, type, duration }]
function show(message, type = 'success', duration = 4000) {
const id = Date.now() + Math.random()
toasts.value.push({ id, message, type, duration })
if (duration > 0) setTimeout(() => dismiss(id), duration)
}
function dismiss(id) {
toasts.value = toasts.value.filter(t => t.id !== id)
}
return { toasts, show, dismiss }
})
```
**Type → visual classes (D-02):**
| type | accent (left-bar) | icon name | icon color |
|------|-------------------|-----------|------------|
| success | bg-green-500 | checkCircle | text-green-500 |
| error | bg-red-500 | exclamationCircle | text-red-500 |
| warning | bg-amber-400 | warning | text-amber-500 |
| info | bg-sky-400 | exclamationCircle | text-sky-500 |
**Container layout (D-01, D-03):**
- Teleport target: `to="body"`
- Wrapper: `fixed bottom-4 right-4 z-[9999] flex flex-col-reverse gap-2 pointer-events-none`
- Each toast: `pointer-events-auto flex items-center gap-3 bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden max-w-sm min-w-[280px]` with `@click="dismiss(toast.id)"`
**App.vue mount point (current state):**
- App.vue uses `