Files
kite/.planning/milestones/v0.2-phases/10-ux-interaction/10-04-PLAN.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
10-ux-interaction 04 execute 0
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
true
UX-10
truths artifacts key_links
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)
path provides contains
frontend/src/stores/toast.js Reactive toast store with toasts array + show + dismiss toasts
path provides
frontend/src/components/ui/ToastContainer.vue Visual renderer for the toast stack
path provides
frontend/src/stores/__tests__/toast.test.js Toast store tests (timers + signature)
path provides
frontend/src/components/ui/__tests__/ToastContainer.test.js ToastContainer rendering tests
from to via pattern
App.vue ToastContainer.vue <ToastContainer /> mounted alongside <router-view> <ToastContainer
from to via pattern
ToastContainer.vue useToastStore store.toasts subscription toastStore.toasts
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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 **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>