Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ Provide concise guardrails so every contribution keeps the SvelteKit app stable,
- Name `.svelte` files in kebab-case even when the exported component stays PascalCase.
- Keep UI primitives self-contained; only split supporting files when there is a real reuse boundary.

#### Directory Responsibilities

- `src/routes/` – Feature entry points; keep files thin and delegate logic/UI to `@/components` and `@/lib`.
- `src/components/ui/` – Source of truth for shadcn primitives defined in `components.json`; extend via props/variants instead of recreating components elsewhere.
- `src/components/<feature>/` – Feature-scoped compositions that stitch primitives together; avoid business logic here.
- `src/lib/` – Shared domain logic, state helpers, schemas, and utilities usable across routes; never import UI from here.
- `src/lib/server/` – Server-only helpers (DB, integrations). Client bundles must not import from this tree.
- `src/lib/server/db/` – Drizzle schema + migrations helpers; any schema change must pair with `db:generate` and `db:migrate`.

### Svelte & Reactivity

- Use Svelte 5 runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`).
Expand Down
76 changes: 76 additions & 0 deletions memory/plans/2026-02-03-team-feedback-toasts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Plan: team-feedback-toasts

Requirement: `memory/requirements/2026-02-03-team-feedback-toasts-requirement.md`
Status: READY_TO_EXECUTE
Owner: Planner

> **How to use**: Author tasks so they remain stack-agnostic. Mark placeholders like `{FRAMEWORK_CMD}` or `{API_ENDPOINT}` where the project must inject specifics.

## Pre-flight Checklist

- Confirm requirement assumptions resolved? TODO:CONFIRM_FLOW_LIST, TODO:COPY_TONE, and TODO:TOAST_THEMING remain open; default to descriptive copy and top-right desktop / bottom-center mobile positioning unless stakeholders redirect.
- Repo/branch prepared? (`git status`, dependencies installed)
- Skills required: [ui-system-principles]
- Tooling commands verified or tagged as needed (`npm run check`, `npm run lint`)

## Tasks

### Task 1: Mount global Sonner toaster

- **Goal**: Install `svelte-sonner` and add a single `<Toaster>` in `src/routes/+layout.svelte` so every page can surface toast feedback that honors shadcn tokens and TODO:TOAST_THEMING (top-right desktop, bottom-center mobile, `closeButton`, duration defaults).
- **Files/Areas**: `package.json`, `package-lock.json`, `src/routes/+layout.svelte`, `src/app.d.ts` (if module augmentation is required), `{SVELTEKIT_APP}` theme tokens consumed by Toaster class overrides.
- **Verify**: `npm run check`
- **Dependencies**: None
- **Rollback**: Remove the `svelte-sonner` dependency and delete the Toaster import/render block from the layout.
- **Notes**: Ensure Toaster inherits existing CSS variables so it matches shadcn visual language; verify it persists across navigations per requirement.

### Task 2: Provide toast helper utilities

- **Goal**: Create `src/lib/ui/toast.ts` (or equivalent) exporting `toastSuccess`, `toastError`, and `toastPromise` wrappers with descriptive copy defaults (per TODO:COPY_TONE) and shared icon/duration rules.
- **Files/Areas**: `src/lib/ui/toast.ts`, `src/lib/ui/index.ts` (if you re-export), `{TEAM_API_ROUTES}` call sites documentation for helper usage examples.
- **Verify**: `npm run lint`
- **Dependencies**: Task 1
- **Rollback**: Delete the helper module and remove any new exports/imports referencing it.
- **Notes**: Centralize copy tokens/constants for reuse; document TODO:CONFIRM_FLOW_LIST items so missing flows can be added later without diverging patterns.

### Task 3: Instrument dashboard route actions

- **Goal**: Wrap all team/variant mutations in `src/routes/+page.svelte` with the helper functions so create/delete collection, rename, duplicate, and destructive confirmation flows surface toast success/error without duplicates.
- **Files/Areas**: `src/routes/+page.svelte`, related form actions under `src/routes/+page.server.ts` if they emit status, `@/lib/stores/team-replace-context` if route-level stores need toast awareness.
- **Verify**: `{QA_SCENARIO}` covering dashboard CRUD triggers Sonner toasts + `npm run check`
- **Dependencies**: Tasks 1-2
- **Rollback**: Revert toast helper calls in the dashboard route to restore previous behavior.
- **Notes**: Prefer `toastPromise` for long-running fetches, ensure copy references the correct entity names, and avoid duplicate toasts by short-circuiting on race conditions.

### Task 4: Add builder-level toast coverage

- **Goal**: Update `src/components/team/team-builder.svelte` (and related builder helpers) to emit toast feedback for duplicate, rename, Pokemon add/remove/update, and all `/api/teams/...` fetch results currently silent.
- **Files/Areas**: `src/components/team/team-builder.svelte`, `@/lib/stores/team-replace-context.ts`, builder-specific helpers under `src/lib/teams/*`.
- **Verify**: `{QA_SCENARIO}` exercising duplicate/rename/add/remove in the builder + `npm run check`
- **Dependencies**: Tasks 1-3
- **Rollback**: Remove the injected helper calls from builder files.
- **Notes**: Ensure promise toasts resolve before navigation (`goto`) executes to keep UX consistent; respect TODO:TOAST_THEMING for different viewports.

### Task 5: Cover Pokemon slot components

- **Goal**: Extend `{TEAM_POKEMON_COMPONENTS}` such as `PokemonDetails` and `PokemonSearch` to emit contextual success/error toasts for slot updates, stat edits, and failed fetch operations instead of silent console logs.
- **Files/Areas**: `src/components/team/pokemon-details.svelte`, `src/components/team/pokemon-search.svelte`, any shared Pokemon CRUD helpers in `src/lib/teams/*`.
- **Verify**: `{QA_SCENARIO}` running Pokemon slot CRUD in desktop + mobile widths (ensuring bottom-center positioning on narrow viewports)
- **Dependencies**: Tasks 1-4
- **Rollback**: Remove toast helper imports/calls from Pokemon components.
- **Notes**: Keep helper copy descriptive per TODO:COPY_TONE; dedupe toasts when multiple slots update simultaneously to mitigate the rapid-fire risk noted in the requirement.

### Task 6: Final QA and regression sweep

- **Goal**: Validate that all async flows emit exactly one toast per success/failure, Sonner theming matches shadcn tokens, and no lint/type regressions were introduced.
- **Files/Areas**: Whole repo (focus on mutated routes/components), test plans in `{QA_SCENARIO}` docs.
- **Verify**: `npm run check`, `npm run lint`, and manual `{QA_SCENARIO}` walkthrough on desktop + mobile widths ensuring Toaster persists across navigation.
- **Dependencies**: Tasks 1-5
- **Rollback**: If regressions appear, revert the specific helper usage or re-run Task 5 → Task 3 changes selectively.
- **Notes**: Capture any remaining TODO resolution needs (e.g., copy/positioning decisions) and escalate if blockers persist.

## Dependencies & Parallelism

- Sequential: Task 1 → Task 2 → Task 3 → Task 4 → Task 5 → Task 6.
- Parallel groups: None (each step builds on Sonner + helper foundations).
- External blockers: Await decisions for TODO:CONFIRM_FLOW_LIST, TODO:COPY_TONE, TODO:TOAST_THEMING if stakeholders override the documented defaults.
70 changes: 70 additions & 0 deletions memory/requirements/2026-02-03-team-feedback-toasts-requirement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Requirement: team-feedback-toasts

Date: 2026-02-03
Status: READY_FOR_PLAN
Owner: Researcher

> **How to use**: Keep placeholders (e.g., `{FRAMEWORK}`, `{API_NAME}`) so this document can be cloned for any repo. Replace them only when you specialize for a concrete project.

## Problem

- User Story: "As a team builder managing variants in `{SVELTEKIT_APP}`, I need immediate toast feedback whenever actions like duplicate, rename, delete, or Pokemon updates run so I know what succeeded or failed before navigation changes hide the UI state."
- Desired Outcome: Every async mutation in the team dashboard and builder surfaces a Sonner toast (success/error) that matches the shadcn visual language and acknowledges the action (e.g., "Variant duplicated"), especially flows that currently feel silent such as duplicate.
- Success Metric: `{QA_SCENARIO}` can trigger team/variant CRUD + Pokemon slot mutations and confirm `toast.*` fired for success/failure 100% of the time without duplicate toasts; UX review signs off once Sonner toasts render in layout across desktop/mobile.

## Solution Overview

- Key Idea: Install and use the shadcn-sanctioned `svelte-sonner` component so a single `<Toaster>` lives in `src/routes/+layout.svelte`, then wrap common toast helpers (success/error/promise) that downstream actions (`handleDuplicate`, `handleCreateCollection`, `handleUpdatePokemon`, etc.) call right after each fetch resolves or throws.
- System Boundary: Root layout for mounting Sonner, the dashboard route `src/routes/+page.svelte`, the variant builder `src/components/team/team-builder.svelte`, and any helper utilities that orchestrate Pokemon CRUD (`PokemonDetails`, `PokemonSearch`, `{TEAM_API_ROUTES}`).
- Assumptions to Validate: `svelte-sonner` styles can inherit existing CSS variables without extra overrides; promise-based toasts won't conflict with `goto` navigation; we can centralize copy strings in one helper so translations stay consistent.

## Scope of Changes

Files/modules to modify (use placeholders where needed):

- `package.json` + lockfile — Add `svelte-sonner` per https://www.shadcn-svelte.com/docs/components/sonner and wire any Vite typings if needed.
- `src/routes/+layout.svelte` — Import `{ Toaster }` from `svelte-sonner`, render it once (top-right, `closeButton`, themed via existing tokens) so all pages can push toasts.
- `src/lib/ui/toast.ts` (or similar) — Export thin helpers around `toast` (`toastSuccess`, `toastError`, `toastPromise`) with opinionated defaults (icon, duration, class) aligned with shadcn tokens.
- `src/routes/+page.svelte` — Wrap team/variant CRUD fetches in `toast.promise` or explicit success/error notifications (create/delete collection, create variant, rename, destructive confirmations) so the dashboard reflects background work.
- `src/components/team/team-builder.svelte` — Surface toasts for duplicate, rename, Pokemon add/update/remove flows, and any API failures coming from `fetch('/api/teams/...')` handlers.
- `{TEAM_POKEMON_COMPONENTS}` (e.g., `PokemonDetails`, `PokemonSearch`) — Trigger contextual successes for slot updates or show failure toast when `fetch` rejects instead of silent console errors.

New files/modules:

- `src/lib/ui/toast.ts` — Centralize Sonner helper exports so route/components import a single source of truth for toast variants/copy.

## Existing References

- Similar pattern: There is no toast system yet, but `src/components/ui/dialog` + shadcn primitives demonstrate how shared UI elements live under `src/components/ui/` with Tailwind tokens; mirror that structure for Sonner helpers.
- Reusable utilities/components: `Button`, `Input`, `Dialog`, `TeamBuilder`, `PokemonDetails`, and the store in `@/lib/stores/team-replace-context` already manage async flows where toast calls should be inserted.
- External dependencies: New dependency on `svelte-sonner`; existing fetch endpoints under `/api/teams/*` provide success/error states to reflect.

## Data Flow / Contracts

- Inputs: User actions on dashboard/builder buttons (create/duplicate/delete teams or variants, edit names, replace Pokemon, remove Pokemon).
- Processing: Each handler already performs `fetch` calls to `/api/teams/...`; add toast helper calls that observe the promise, display loading states when appropriate, and handle thrown errors.
- Outputs: Toast UI rendered by `<Toaster>` acknowledges completion/failure; navigations triggered by `goto` should happen only after success to avoid flashing stale states; state stores update as they do now.
- Schema impacts: None—this work sits entirely in the UI layer; just ensure toast copy references existing entity names without requiring schema changes.

## Compliance With `AGENTS.md`

- Standards touched: Keep Svelte 5 runes, reuse shadcn primitives, ensure Sonner styling matches defined CSS variables, and rerun `npm run check` + `npm run lint` after wiring toasts.
- Deviations: None expected; Sonner is already a documented shadcn component, so no custom toast systems should be introduced.

## Edge Cases & Risks

- Rapid-fire actions (e.g., spamming duplicate) could queue multiple toasts; need rate-limiting or deduping copy to avoid noise.
- Navigations immediately after a toast might unmount components; ensure the global `<Toaster>` persists so the toast remains visible even when routes change.
- Network failures currently only log to console; forgetting to add `toast.error` leaves the UX unchanged—auditing every fetch path is critical.

## Out of Scope

- Rewriting API endpoints or adding optimistic updates beyond toast feedback.
- Designing a custom notification center or persistence for historical toasts; Sonner transient feedback is sufficient.
- Introducing non-shadcn notification libraries; requirement is to stick with Sonner.

## Open Questions

- TODO:CONFIRM_FLOW_LIST — Which exact flows beyond duplicate need toasts (e.g., Pokemon stat edits, Name edits, new variant navigation) before implementation? all those.
- TODO:COPY_TONE — Should success/error copy follow a playful tone ("Team duplicated!"), or stay strictly descriptive for enterprise feel? strictly descriptive
- TODO:TOAST_THEMING — Do we want global positioning/duration defaults (top-right, 4s) or scenario-specific overrides like sticky destructive toasts for deletions? ye, but for mobile shoud be bottom-center
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.4",
"nanoid": "^5.1.6",
"svelte-sonner": "^1.0.7",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
Expand Down
39 changes: 36 additions & 3 deletions src/components/team/pokemon-details.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import TypeBadge from '@/components/ui/type-badge.svelte';
import { X, Ban, Lock, Trash2 } from '@lucide/svelte';
import { useTeamReplaceContext } from '@/lib/stores/team-replace-context';
import type { Stat } from '@/lib/pokemon/stat';
import { STAT_LABELS, type Stat } from '@/lib/pokemon/stat';
import { toastSuccess } from '@/components/ui/toast';

let { pokemon, onClose, onRemove, onUpdate } = $props<{
pokemon: TeamPokemon;
Expand Down Expand Up @@ -47,6 +48,8 @@
speed: 'spe'
};

const STAT_KEYS: Stat[] = ['hp', 'atk', 'def', 'spA', 'spD', 'spe'];

let fullData = $state<PokemonData | null>(null);
let activeTab = $state<'stats' | 'build' | 'moves'>('stats');
let loading = $state(true);
Expand All @@ -60,9 +63,10 @@
loading = true;
try {
const res = await fetch(`/api/pokemon/${pokemon.pokemonId}`);
if (res.ok) {
fullData = await res.json();
if (!res.ok) {
throw new Error('Failed to load Pokémon details');
}
fullData = (await res.json()) as PokemonData;
} catch {
fullData = null;
} finally {
Expand Down Expand Up @@ -114,6 +118,31 @@
spe: pokemon.ivSpe ?? 31
});

const listStats = (stats: Stat[]) => stats.map((stat) => STAT_LABELS[stat]).join(', ');

const describeStatChange = (type: 'ev' | 'iv', changed: Stat[], values: Record<Stat, number>) => {
const label = type === 'ev' ? 'EVs' : 'IVs';
if (changed.length === 1) {
const stat = changed[0];
return `${STAT_LABELS[stat]} ${label} set to ${values[stat]}`;
}
if (changed.length === STAT_KEYS.length) {
return `${label} reset`;
}
return `${label} updated (${listStats(changed)})`;
};

const dispatchStatToast = (type: 'ev' | 'iv', changed: Stat[], values: Record<Stat, number>) => {
if (!changed.length) return;
const slotLabel = pokemon.slot ? `Slot ${pokemon.slot}` : 'Current slot';
toastSuccess({
id: `${type}-spread-slot-${pokemon.slot}`,
title: type === 'ev' ? 'EV spread saved' : 'IV spread saved',
description: `${slotLabel}: ${describeStatChange(type, changed, values)}`,
duration: 3800
});
};

const currentEvs = $derived({
hp: pokemon.evHp ?? 0,
atk: pokemon.evAtk ?? 0,
Expand All @@ -140,6 +169,7 @@
};

const handleEvsChange = (evs: Record<Stat, number>) => {
const changedStats = STAT_KEYS.filter((stat) => currentEvs[stat] !== evs[stat]);
onUpdate({
...pokemon,
evHp: evs.hp,
Expand All @@ -149,9 +179,11 @@
evSpD: evs.spD,
evSpe: evs.spe
});
dispatchStatToast('ev', changedStats, evs);
};

const handleIvsChange = (ivs: Record<Stat, number>) => {
const changedStats = STAT_KEYS.filter((stat) => currentIvs[stat] !== ivs[stat]);
onUpdate({
...pokemon,
ivHp: ivs.hp,
Expand All @@ -161,6 +193,7 @@
ivSpD: ivs.spD,
ivSpe: ivs.spe
});
dispatchStatToast('iv', changedStats, ivs);
};

const handleHeldItemChange = (item: { id: number; name: string; sprite: string } | null) => {
Expand Down
Loading