# Story 3.2: Composant NarratorBubble (Le Bug) Status: ready-for-dev ## Story As a visiteur, I want voir un narrateur-guide qui m'accompagne dans mon exploration, so that je me sens guidé et l'expérience est immersive. ## Acceptance Criteria 1. **Given** le composant `NarratorBubble` est implémenté **When** le narrateur doit afficher un message **Then** une bulle apparaît en bas de l'écran (desktop) ou au-dessus de la bottom bar (mobile) 2. **And** l'avatar du Bug (araignée) s'affiche avec son apparence selon le `narratorStage` du store 3. **And** le texte apparaît avec effet typewriter (lettre par lettre) 4. **And** un clic ou Espace accélère l'animation typewriter 5. **And** la bulle peut être fermée/minimisée sans bloquer la navigation 6. **And** le composant utilise `aria-live="polite"` et `role="status"` pour l'accessibilité 7. **And** `prefers-reduced-motion` affiche le texte instantanément 8. **And** la police serif narrative est utilisée pour le texte 9. **And** l'animation d'apparition/disparition est fluide et non-bloquante ## Tasks / Subtasks - [ ] **Task 1: Créer le composable useTypewriter** (AC: #3, #4, #7) - [ ] Créer `frontend/app/composables/useTypewriter.ts` - [ ] Accepter le texte en paramètre - [ ] Afficher lettre par lettre (30-50ms par lettre) - [ ] Exposer une méthode `skip()` pour afficher tout le texte instantanément - [ ] Respecter `prefers-reduced-motion` - [ ] **Task 2: Créer les assets du Bug par stage** (AC: #2) - [ ] Préparer 5 images SVG ou PNG pour les 5 stades du Bug - [ ] Stage 1 : silhouette sombre floue - [ ] Stage 2 : forme vague avec yeux - [ ] Stage 3 : pattes visibles - [ ] Stage 4 : araignée reconnaissable - [ ] Stage 5 : mascotte complète révélée - [ ] Placer dans `frontend/public/images/bug/` - [ ] **Task 3: Créer le composant NarratorBubble** (AC: #1, #2, #3, #4, #5, #8, #9) - [ ] Créer `frontend/app/components/feature/NarratorBubble.vue` - [ ] Props : message (string), visible (boolean) - [ ] Emit : close, skip - [ ] Afficher l'avatar du Bug selon `narratorStage` du store - [ ] Intégrer le composable useTypewriter - [ ] Bouton de fermeture/minimisation - [ ] Utiliser font-narrative pour le texte - [ ] **Task 4: Implémenter l'accessibilité** (AC: #6, #7) - [ ] Ajouter `aria-live="polite"` sur le conteneur - [ ] Ajouter `role="status"` pour signaler les mises à jour - [ ] S'assurer que le texte complet est accessible même pendant l'animation - [ ] Tester avec prefers-reduced-motion - [ ] **Task 5: Animation d'apparition/disparition** (AC: #9) - [ ] Slide-up pour l'apparition - [ ] Fade-out pour la disparition - [ ] Utiliser CSS transitions pour fluidité - [ ] Non-bloquante : ne pas empêcher les interactions avec le reste de la page - [ ] **Task 6: Responsive design** (AC: #1) - [ ] Desktop : bulle en bas de l'écran (position fixed) - [ ] Mobile : au-dessus de la bottom bar (variable CSS pour le spacing) - [ ] Taille adaptée à l'écran - [ ] **Task 7: Tests et validation** - [ ] Tester l'effet typewriter - [ ] Tester le skip au clic/Espace - [ ] Vérifier les 5 stades du Bug - [ ] Valider l'accessibilité (screen reader) - [ ] Tester prefers-reduced-motion - [ ] Valider responsive (desktop/mobile) ## Dev Notes ### Composable useTypewriter ```typescript // frontend/app/composables/useTypewriter.ts export interface UseTypewriterOptions { speed?: number // ms par caractère onComplete?: () => void } export function useTypewriter(options: UseTypewriterOptions = {}) { const { speed = 40, onComplete } = options const text = ref('') const displayedText = ref('') const isTyping = ref(false) const isComplete = ref(false) const reducedMotion = useReducedMotion() let intervalId: ReturnType | null = null let currentIndex = 0 function start(newText: string) { text.value = newText displayedText.value = '' currentIndex = 0 isTyping.value = true isComplete.value = false // Si prefers-reduced-motion, afficher tout instantanément if (reducedMotion.value) { skip() return } intervalId = setInterval(() => { if (currentIndex < text.value.length) { displayedText.value += text.value[currentIndex] currentIndex++ } else { complete() } }, speed) } function skip() { if (intervalId) { clearInterval(intervalId) intervalId = null } displayedText.value = text.value complete() } function complete() { if (intervalId) { clearInterval(intervalId) intervalId = null } isTyping.value = false isComplete.value = true onComplete?.() } function reset() { if (intervalId) { clearInterval(intervalId) intervalId = null } text.value = '' displayedText.value = '' currentIndex = 0 isTyping.value = false isComplete.value = false } onUnmounted(() => { if (intervalId) { clearInterval(intervalId) } }) return { text, displayedText, isTyping, isComplete, start, skip, reset, } } ``` ### 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 reducedMotion } ``` ### Composant NarratorBubble ```vue ``` ### Clés i18n à ajouter **fr.json :** ```json { "narrator": { "clickToSkip": "Cliquez ou appuyez sur Espace pour passer" } } ``` **en.json :** ```json { "narrator": { "clickToSkip": "Click or press Space to skip" } } ``` ### Structure des assets du Bug ``` frontend/public/images/bug/ ├── bug-stage-1.svg # Silhouette sombre floue ├── bug-stage-2.svg # Forme vague avec yeux ├── bug-stage-3.svg # Pattes visibles ├── bug-stage-4.svg # Araignée reconnaissable └── bug-stage-5.svg # Mascotte complète révélée ``` ### Utilisation du composant ```vue ``` ### Dépendances **Cette story nécessite :** - Story 3.1 : API narrateur pour les textes - Story 1.6 : Store Pinia (pour narratorStage) **Cette story prépare pour :** - Story 3.3 : Textes contextuels (utilise ce composant) - Story 3.5 : Logique de progression (déclenche le narrateur) ### Project Structure Notes **Fichiers à créer :** ``` frontend/app/ ├── components/feature/ │ └── NarratorBubble.vue # CRÉER ├── composables/ │ ├── useTypewriter.ts # CRÉER │ └── useReducedMotion.ts # CRÉER └── public/images/bug/ ├── bug-stage-1.svg # CRÉER (asset) ├── bug-stage-2.svg # CRÉER (asset) ├── bug-stage-3.svg # CRÉER (asset) ├── bug-stage-4.svg # CRÉER (asset) └── bug-stage-5.svg # CRÉER (asset) ``` **Fichiers à modifier :** ``` frontend/i18n/fr.json # AJOUTER narrator.clickToSkip frontend/i18n/en.json # AJOUTER narrator.clickToSkip ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-3.2] - [Source: docs/planning-artifacts/ux-design-specification.md#NarratorBubble] - [Source: docs/planning-artifacts/ux-design-specification.md#Narrator-Revelation-Arc] - [Source: docs/brainstorming-gamification-2026-01-26.md#Mascotte-Le-Bug] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | Effect typewriter | 30-50ms par lettre | Epics | | Stades du Bug | 5 apparences distinctes | UX Spec | | Position desktop | Bottom fixed | Epics | | Position mobile | Au-dessus bottom bar | Epics | | Accessibilité | aria-live + role="status" | Epics | | Police | font-narrative | UX Spec | ## 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