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>
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 |
|
true |
|
|
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— reactivetoastsarray,showaction (auto-dismiss via setTimeout),dismissactioncomponents/ui/ToastContainer.vue— Teleport-to-body container with TransitionGroupApp.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 && 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>