# Story 2.7: Composant Dialogue PNJ Status: ready-for-dev ## Story As a visiteur, I want lire les témoignages comme des dialogues de personnages style Zelda, so that l'expérience est immersive et mémorable. ## Acceptance Criteria 1. **Given** le composant `DialoguePNJ` est implémenté **When** il reçoit les données d'un témoignage en props **Then** l'avatar du PNJ s'affiche à gauche avec un style illustratif 2. **And** une bulle de dialogue s'affiche à droite avec le texte 3. **And** l'effet typewriter fait apparaître le texte lettre par lettre 4. **And** un clic ou appui sur Espace accélère l'animation typewriter (x3-x5) 5. **And** la personnalité du PNJ influence le style visuel de la bulle (sage, sarcastique, enthousiaste, professionnel) 6. **And** la police serif narrative est utilisée pour le texte du dialogue 7. **And** `prefers-reduced-motion` affiche le texte complet instantanément 8. **And** le texte complet est accessible via `aria-label` pour les screen readers 9. **And** une navigation entre témoignages est disponible (précédent/suivant) 10. **And** une transition animée s'effectue entre les PNJ 11. **And** un indicateur du témoignage actuel est visible (ex: 2/5) 12. **And** la navigation au clavier est fonctionnelle (flèches gauche/droite) ## Tasks / Subtasks - [ ] **Task 1: Créer le composant DialoguePNJ** (AC: #1, #2, #5, #6) - [ ] Créer `frontend/app/components/feature/DialoguePNJ.vue` - [ ] Props : testimonials (array), initialIndex (number) - [ ] Layout : avatar à gauche, bulle de dialogue à droite - [ ] Styles différents selon personality - [ ] **Task 2: Implémenter l'effet typewriter** (AC: #3, #4) - [ ] Créer un composable `useTypewriter` pour l'animation - [ ] Afficher le texte lettre par lettre (vitesse ~30-50ms) - [ ] Clic ou Espace accélère l'animation (x3-x5) - [ ] État : "typing" ou "complete" - [ ] **Task 3: Gérer prefers-reduced-motion** (AC: #7) - [ ] Détecter la préférence via media query - [ ] Si activé, afficher le texte complet instantanément - [ ] Créer un composable `useReducedMotion()` - [ ] **Task 4: Accessibilité** (AC: #8) - [ ] Ajouter `aria-label` avec le texte complet - [ ] `role="article"` sur le conteneur de dialogue - [ ] `aria-live="polite"` pour annoncer les changements - [ ] **Task 5: Navigation entre témoignages** (AC: #9, #10, #11, #12) - [ ] Boutons précédent/suivant - [ ] Indicateur de position (2/5) - [ ] Transition animée entre les PNJ (fade/slide) - [ ] Navigation clavier : flèches gauche/droite - [ ] Focus trap sur le composant - [ ] **Task 6: Intégrer dans la page Témoignages** (AC: tous) - [ ] Remplacer les TestimonialCards par DialoguePNJ - [ ] Mode "dialogue" pour l'expérience immersive - [ ] Option pour revenir à la vue "liste" - [ ] **Task 7: Styles visuels par personnalité** (AC: #5) - [ ] sage : bulle bleutée, bordure calme - [ ] sarcastique : bulle violacée, italique - [ ] enthousiaste : bulle orange accent, texte dynamique - [ ] professionnel : bulle grise, sobre - [ ] **Task 8: Tests et validation** - [ ] Tester l'effet typewriter - [ ] Valider l'accélération au clic/Espace - [ ] Tester prefers-reduced-motion - [ ] Valider la navigation clavier - [ ] Vérifier l'accessibilité avec screen reader ## Dev Notes ### Composable useTypewriter ```typescript // frontend/app/composables/useTypewriter.ts export interface UseTypewriterOptions { text: string speed?: number // ms entre chaque caractère speedMultiplier?: number // facteur d'accélération } export function useTypewriter(options: UseTypewriterOptions) { const { text, speed = 40, speedMultiplier = 5 } = options const displayedText = ref('') const isTyping = ref(true) const isAccelerated = ref(false) let timeoutId: NodeJS.Timeout | null = null let currentIndex = 0 const reducedMotion = useReducedMotion() function typeNextChar() { if (currentIndex < text.length) { displayedText.value += text[currentIndex] currentIndex++ const currentSpeed = isAccelerated.value ? speed / speedMultiplier : speed timeoutId = setTimeout(typeNextChar, currentSpeed) } else { isTyping.value = false } } function start() { if (reducedMotion.value) { // Afficher tout le texte immédiatement displayedText.value = text isTyping.value = false return } displayedText.value = '' currentIndex = 0 isTyping.value = true isAccelerated.value = false typeNextChar() } function accelerate() { isAccelerated.value = true } function skip() { if (timeoutId) clearTimeout(timeoutId) displayedText.value = text isTyping.value = false } function reset() { if (timeoutId) clearTimeout(timeoutId) displayedText.value = '' currentIndex = 0 isTyping.value = true isAccelerated.value = false } onMounted(() => { start() }) onUnmounted(() => { if (timeoutId) clearTimeout(timeoutId) }) return { displayedText: readonly(displayedText), isTyping: readonly(isTyping), accelerate, skip, reset, start, } } ``` ### Composable useReducedMotion ```typescript // frontend/app/composables/useReducedMotion.ts export function useReducedMotion() { const reducedMotion = ref(false) onMounted(() => { const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)') reducedMotion.value = mediaQuery.matches const handler = (e: MediaQueryListEvent) => { reducedMotion.value = e.matches } mediaQuery.addEventListener('change', handler) onUnmounted(() => { mediaQuery.removeEventListener('change', handler) }) }) return readonly(reducedMotion) } ``` ### Composant DialoguePNJ ```vue ``` ### Modification de la page Témoignages ```vue ``` ### Clés i18n nécessaires **fr.json :** ```json { "testimonials": { "clickToContinue": "Cliquez ou appuyez sur Espace pour continuer...", "previous": "Précédent", "next": "Suivant", "finish": "Terminer", "keyboardHint": "Utilisez les flèches ← → pour naviguer, Espace pour accélérer", "dialogueMode": "Dialogue", "listMode": "Liste" } } ``` **en.json :** ```json { "testimonials": { "clickToContinue": "Click or press Space to continue...", "previous": "Previous", "next": "Next", "finish": "Finish", "keyboardHint": "Use ← → arrows to navigate, Space to speed up", "dialogueMode": "Dialogue", "listMode": "List" } } ``` ### Dépendances **Cette story nécessite :** - Story 2.6 : Table testimonials, API, type Testimonial **Cette story prépare pour :** - Story 3.2 : NarratorBubble (pattern similaire typewriter) ### Project Structure Notes **Fichiers à créer :** ``` frontend/app/ ├── components/feature/ │ └── DialoguePNJ.vue # CRÉER └── composables/ ├── useTypewriter.ts # CRÉER └── useReducedMotion.ts # CRÉER ``` **Fichiers à modifier :** ``` frontend/app/pages/temoignages.vue # MODIFIER pour intégrer DialoguePNJ frontend/i18n/fr.json # AJOUTER clés frontend/i18n/en.json # AJOUTER clés ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.7] - [Source: docs/planning-artifacts/ux-design-specification.md#DialoguePNJ] - [Source: docs/planning-artifacts/ux-design-specification.md#Accessibility-Strategy] - [Source: docs/planning-artifacts/ux-design-specification.md#Typography-System] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | Typewriter speed | 30-50ms par caractère | UX Spec | | Accélération | x3-x5 | Epics | | Police | font-narrative (serif) | UX Spec | | prefers-reduced-motion | Texte instantané | NFR6 | | Accessibilité | aria-label, keyboard nav | WCAG AA | ## Dev Agent Record ### Agent Model Used {{agent_model_name_version}} ### Debug Log References ### Completion Notes List ### Change Log | Date | Change | Author | |------|--------|--------| | 2026-02-04 | Story créée avec contexte complet | SM Agent | ### File List