---
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: "<ToastContainer /> mounted alongside <router-view>"
      pattern: "<ToastContainer"
    - from: "ToastContainer.vue"
      to: "useToastStore"
      via: "store.toasts subscription"
      pattern: "toastStore\\.toasts"
---

<objective>
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 `<ToastContainer />`
- Vitest tests for both store and container
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@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

<interfaces>
**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 `<script setup>` (Composition API exception per CLAUDE.md)
- `<router-view />` lives inside `<main>` for non-auth routes
- Add `<ToastContainer />` AFTER the layout `<div>` so it floats over all routes including AuthLayout
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Write failing tests for toast store + ToastContainer</name>
  <files>frontend/src/stores/__tests__/toast.test.js, frontend/src/components/ui/__tests__/ToastContainer.test.js</files>
  <read_first>
    - frontend/src/stores/toast.js (current Phase 8 stub)
    - frontend/src/stores/__tests__/cloudConnections.test.js (Pinia test setup pattern)
    - .planning/phases/10-ux-interaction/10-PATTERNS.md §"toast.js" and §"ToastContainer.vue"
  </read_first>
  <behavior>
    **toast.test.js (6 tests):**
    - Test 1: `show(msg, type, duration) appends a toast with the given fields` — setActivePinia + create store, call show('Hi', 'success', 4000), assert store.toasts.length === 1 and the toast has message='Hi', type='success', duration=4000
    - Test 2: `show defaults type to 'success' and duration to 4000` — call show('Hi'), assert toasts[0].type === 'success' and toasts[0].duration === 4000
    - Test 3: `auto-dismisses after duration using fake timers` — use vi.useFakeTimers(), call show('Hi', 'success', 4000), assert length===1, advance time by 4000ms via vi.advanceTimersByTime(4000), assert length===0
    - Test 4: `dismiss(id) removes the toast immediately` — show, capture id from toasts[0].id, call dismiss(id), assert length===0
    - Test 5: `duration=0 disables auto-dismiss` — vi.useFakeTimers(), show('Hi', 'success', 0), advance 10000ms, assert length still === 1
    - Test 6: `multiple toasts stack with unique ids` — call show three times, assert toasts.length === 3 and all ids are distinct

    **ToastContainer.test.js (4 tests):**
    - Test 1: `renders nothing when store.toasts is empty` — mount with empty store, assert wrapper.find('[data-test="toast"]').exists() === false (or assert no .bg-white toast cards)
    - Test 2: `renders one element per toast` — populate store with 2 toasts, mount, assert wrapper.findAll('[data-test="toast"]').length === 2 (or count by class)
    - Test 3: `clicking a toast calls dismiss(id)` — populate store with 1 toast, mount, click the toast, assert store.toasts.length === 0
    - Test 4: `accent class matches type (success → bg-green-500)` — populate store with 1 success toast, mount, find the accent bar element, assert its classList includes 'bg-green-500'
  </behavior>
  <action>
    Create both test files.

    For `frontend/src/stores/__tests__/toast.test.js`: use `import { setActivePinia, createPinia } from 'pinia'` and `beforeEach(() => setActivePinia(createPinia()))`. Use `vi.useFakeTimers()` / `vi.useRealTimers()` for timer tests.

    For `frontend/src/components/ui/__tests__/ToastContainer.test.js`: setActivePinia + createPinia in beforeEach. Mount ToastContainer with `global.stubs: { AppIcon: true, Teleport: true }` (Teleport stub disables the body-teleport so wrapper.find works on the rendered DOM). Add `data-test="toast"` attribute to the toast element template (Task 2). Use a manual fixture data-test selector OR identify the toast via a stable class like `pointer-events-auto`.

    Tests fail until Task 2 implements the store + container.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run toast</automated>
    Expected: tests collected; ~10 failures total across the two files.
  </verify>
  <acceptance_criteria>
    - `frontend/src/stores/__tests__/toast.test.js` exists with 6 `it(...)` blocks
    - `frontend/src/components/ui/__tests__/ToastContainer.test.js` exists with 4 `it(...)` blocks
    - Toast store tests use vi.useFakeTimers() correctly (no flaky time-based assertions)
    - ToastContainer tests stub Teleport and AppIcon
    - Running `cd frontend && npm run test -- --run toast` shows ~10 failing tests
  </acceptance_criteria>
  <done>10 failing tests covering store and container contracts.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement toast.js store + ToastContainer.vue + mount in App.vue</name>
  <files>frontend/src/stores/toast.js, frontend/src/components/ui/ToastContainer.vue, frontend/src/App.vue</files>
  <read_first>
    - frontend/src/stores/toast.js (current stub — DO NOT change exported signature)
    - frontend/src/stores/__tests__/toast.test.js (tests from Task 1)
    - frontend/src/components/ui/__tests__/ToastContainer.test.js (tests from Task 1)
    - frontend/src/App.vue (current state — uses <script setup>)
    - frontend/src/components/ui/SearchableModelSelect.vue (Teleport pattern reference)
    - .planning/phases/10-ux-interaction/10-PATTERNS.md §"toast.js" and §"ToastContainer.vue"
    - frontend/src/components/settings/SettingsAccountTab.vue (Phase 8 call site — must keep working)
    - frontend/src/components/auth/TotpEnrollment.vue (Phase 8 call site — must keep working)
  </read_first>
  <behavior>
    1. `stores/toast.js`: setup-store form (D-04 LOCKED — keep `defineStore('toast', () => {...})`). Returns `{ toasts, show, dismiss }`. Signature MUST remain `show(message, type = 'success', duration = 4000)`.
    2. `components/ui/ToastContainer.vue`: Options API, registers `AppIcon`. Computed maps for `accentClass(type)`, `iconName(type)`, `iconColorClass(type)` using the Type table in <interfaces>. Template wraps everything in `<Teleport to="body">` with `<TransitionGroup name="toast">`. Each toast div has `data-test="toast"`, `@click="toastStore.dismiss(toast.id)"`, and includes (in order) a left accent bar div, an `<AppIcon>`, and the `{{ toast.message }}` paragraph.
    3. `App.vue`: Add `import ToastContainer from './components/ui/ToastContainer.vue'` to the `<script setup>` block. Add `<ToastContainer />` to the template — place it AFTER the existing `<div v-else>` so it floats over all routes. Add the `<style>` block (scoped or global) for `.toast-enter-active`, `.toast-enter-from`, `.toast-leave-active`, `.toast-leave-to` providing a simple fade+translate transition.
  </behavior>
  <action>
    **Step A — toast.js:**
    Replace the current stub with the implementation from `10-PATTERNS.md §"toast.js"`. Keep the file structure identical to the stub (default export `defineStore('toast', () => {...})`). Implementation:
    ```js
    import { ref } from 'vue'
    import { defineStore } from 'pinia'

    export const useToastStore = defineStore('toast', () => {
      const toasts = ref([])
      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 }
    })
    ```

    **Step B — ToastContainer.vue:**
    Create using Options API. Methods `accentClass(type)`, `iconName(type)`, `iconColorClass(type)` returning the table values in <interfaces>. The toast item template:
    ```
    <div
      v-for="toast in toastStore.toasts"
      :key="toast.id"
      data-test="toast"
      class="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] cursor-pointer"
      @click="toastStore.dismiss(toast.id)"
    >
      <div class="w-1 self-stretch shrink-0" :class="accentClass(toast.type)"></div>
      <AppIcon :name="iconName(toast.type)" class="w-5 h-5 shrink-0" :class="iconColorClass(toast.type)" />
      <p class="text-sm text-gray-800 flex-1 py-3 pr-4">{{ toast.message }}</p>
    </div>
    ```
    Wrap in `<Teleport to="body"><div class="fixed bottom-4 right-4 z-[9999] flex flex-col-reverse gap-2 pointer-events-none"><TransitionGroup name="toast">...</TransitionGroup></div></Teleport>`.

    Setup the store via `import { useToastStore } from '../../stores/toast.js'` and expose it as `toastStore` in `data() { return { toastStore: useToastStore() } }` (Options API store wiring; project uses relative paths — no `@/` alias configured).

    Add a `<style scoped>` block defining `.toast-enter-active`, `.toast-leave-active { transition: all 0.2s ease }`, `.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateX(20px) }`.

    **Step C — App.vue:**
    Modify App.vue (currently `<script setup>`). Add to imports:
    ```js
    import ToastContainer from './components/ui/ToastContainer.vue'
    ```
    Add to template AFTER the closing `</div>` of the layout wrapper:
    ```
    <ToastContainer />
    ```
    Keep the existing structure (AuthLayout v-if / div v-else / AppSidebar / main / router-view) entirely intact. Do NOT remove or rename anything in App.vue beyond adding the import and the `<ToastContainer />` element.

    NO comments in any file.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run toast &amp;&amp; cd frontend && npm run test -- --run ToastContainer</automated>
    Expected: 10 tests pass (6 store + 4 container). The pre-existing tests `SettingsAccountTab.test.js` and `TotpEnrollment.test.js` continue to pass because the `show` signature is unchanged.
  </verify>
  <acceptance_criteria>
    - `frontend/src/stores/toast.js` contains `ref([])` and `setTimeout(() => dismiss(id), duration)` lines
    - `frontend/src/stores/toast.js` exports `useToastStore` with `{ toasts, show, dismiss }` returned
    - The `show` function signature is unchanged: `show(message, type = 'success', duration = 4000)` (grep: `grep -E "function show\\(message,\\s*type\\s*=\\s*'success',\\s*duration\\s*=\\s*4000\\)" frontend/src/stores/toast.js` returns 1)
    - `frontend/src/components/ui/ToastContainer.vue` exists, imports AppIcon, uses `<Teleport to="body">`, uses `<TransitionGroup name="toast">`, includes `data-test="toast"` attribute on the toast element
    - `frontend/src/App.vue` contains `import ToastContainer` and `<ToastContainer />` in the template (grep: `grep -c "ToastContainer" frontend/src/App.vue` returns ≥ 2)
    - `cd frontend && npm run test -- --run toast` exits 0
    - `cd frontend && npm run test -- --run ToastContainer` exits 0
    - `cd frontend && npm run test -- --run SettingsAccountTab` still exits 0 (Phase 8 regression check)
    - `cd frontend && npm run test -- --run TotpEnrollment` still exits 0 (Phase 8 regression check)
  </acceptance_criteria>
  <done>Toast store reactive, ToastContainer rendering, App.vue mounted, all tests green, Phase 8 sites unchanged.</done>
</task>

</tasks>

<verification>
- `cd frontend && npm run test -- --run toast` exits 0
- `cd frontend && npm run test -- --run ToastContainer` exits 0
- `cd frontend && npm run test -- --run SettingsAccountTab` exits 0 (regression)
- `cd frontend && npm run test -- --run TotpEnrollment` exits 0 (regression)
- `grep -E "show\\(message,\\s*type\\s*=\\s*'success',\\s*duration\\s*=\\s*4000\\)" frontend/src/stores/toast.js` returns 1 (locked signature preserved)
- `grep -E "<ToastContainer" frontend/src/App.vue` returns 1
- `grep -E "Teleport to=\"body\"" frontend/src/components/ui/ToastContainer.vue` returns 1
</verification>

<success_criteria>
The toast system is fully wired and visible across the app. Wave 1 plan 10-09 can add `useToastStore().show('Document moved', 'success')` calls inside `FileManagerView.vue` action handlers and a real toast will appear bottom-right. Phase 8 call sites (`SettingsAccountTab`, `TotpEnrollment`) work unchanged.
</success_criteria>

<output>
Create `.planning/phases/10-ux-interaction/10-04-SUMMARY.md` when done.
</output>
