Setup complet de l'infrastructure projet : - Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS) - Backend Laravel 12 API-only avec middleware X-API-Key et CORS - Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter) - Documentation planning et implementation artifacts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
663 lines
18 KiB
Markdown
663 lines
18 KiB
Markdown
# Story 4.5: Easter eggs - Implémentation UI et collection
|
|
|
|
Status: ready-for-dev
|
|
|
|
## Story
|
|
|
|
As a visiteur curieux,
|
|
I want découvrir des surprises cachées et voir ma collection,
|
|
so that l'exploration est récompensée.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Given** des easter eggs sont placés sur différentes pages **When** le visiteur déclenche un easter egg (clic, hover, konami, scroll, sequence) **Then** une animation de découverte s'affiche (popup, effet visuel)
|
|
2. **And** la récompense est affichée (snippet de code, anecdote, image, badge)
|
|
3. **And** le narrateur réagit avec enthousiasme
|
|
4. **And** une notification "Easter egg trouvé ! (X/Y)" s'affiche
|
|
5. **And** le slug est ajouté à `easterEggsFound` dans le store
|
|
6. **And** un bouton permet de fermer et continuer
|
|
7. **Given** le visiteur accède à sa collection (via paramètres ou zone dédiée) **When** la collection s'affiche **Then** une grille montre les easter eggs trouvés et des silhouettes mystère pour les non-trouvés
|
|
8. **And** les détails sont visibles pour les découverts
|
|
9. **And** un compteur X/Y indique la progression
|
|
10. **And** un badge spécial s'affiche si 100% trouvés
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [ ] **Task 1: Créer le composable useEasterEggDetection** (AC: #1)
|
|
- [ ] Créer `frontend/app/composables/useEasterEggDetection.ts`
|
|
- [ ] Détecter les différents types de triggers
|
|
- [ ] Hook pour écouter le Konami code
|
|
- [ ] Hook pour séquences de clics
|
|
- [ ] Détecter scroll en bas de page
|
|
|
|
- [ ] **Task 2: Créer le composant EasterEggPopup** (AC: #1, #2, #6)
|
|
- [ ] Créer `frontend/app/components/feature/EasterEggPopup.vue`
|
|
- [ ] Modal avec animation de découverte
|
|
- [ ] Afficher la récompense selon le type (snippet, anecdote, image, badge)
|
|
- [ ] Bouton fermer
|
|
|
|
- [ ] **Task 3: Créer le composant EasterEggNotification** (AC: #4)
|
|
- [ ] Créer `frontend/app/components/feature/EasterEggNotification.vue`
|
|
- [ ] Toast notification "Easter egg trouvé ! (X/Y)"
|
|
- [ ] Animation d'apparition/disparition
|
|
- [ ] Position non-bloquante
|
|
|
|
- [ ] **Task 4: Intégrer le narrateur** (AC: #3)
|
|
- [ ] Ajouter contexte `easter_egg_found` dans l'API narrateur
|
|
- [ ] Le narrateur réagit avec enthousiasme
|
|
- [ ] Message différent selon le type de récompense
|
|
|
|
- [ ] **Task 5: Créer le composant EasterEggCollection** (AC: #7, #8, #9, #10)
|
|
- [ ] Créer `frontend/app/components/feature/EasterEggCollection.vue`
|
|
- [ ] Grille d'easter eggs (trouvés vs mystères)
|
|
- [ ] Compteur X/Y
|
|
- [ ] Badge spécial si 100%
|
|
|
|
- [ ] **Task 6: Placer les détecteurs sur les pages** (AC: #1)
|
|
- [ ] Header : araignée cachée (click)
|
|
- [ ] Landing : Konami code
|
|
- [ ] Projets : commentaire secret (hover)
|
|
- [ ] Parcours : scroll bottom + hover date
|
|
- [ ] Compétences : séquence tech
|
|
- [ ] Global : clics logo
|
|
|
|
- [ ] **Task 7: Intégrer dans les paramètres/settings** (AC: #7)
|
|
- [ ] Ajouter un onglet ou section "Collection"
|
|
- [ ] Accessible depuis le drawer des paramètres mobile
|
|
- [ ] Accessible depuis le menu desktop
|
|
|
|
- [ ] **Task 8: Tests et validation**
|
|
- [ ] Tester chaque type de trigger
|
|
- [ ] Vérifier l'affichage des récompenses
|
|
- [ ] Tester la collection
|
|
- [ ] Valider le compteur
|
|
- [ ] Tester le badge 100%
|
|
|
|
## Dev Notes
|
|
|
|
### Composable useEasterEggDetection
|
|
|
|
```typescript
|
|
// frontend/app/composables/useEasterEggDetection.ts
|
|
import type { EasterEggMeta } from './useFetchEasterEggs'
|
|
|
|
interface UseEasterEggDetectionOptions {
|
|
onFound: (slug: string) => void
|
|
}
|
|
|
|
// Konami Code : ↑↑↓↓←→←→BA
|
|
const KONAMI_CODE = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'KeyB', 'KeyA']
|
|
|
|
export function useEasterEggDetection(options: UseEasterEggDetectionOptions) {
|
|
const { fetchList, getByLocation } = useFetchEasterEggs()
|
|
const progressionStore = useProgressionStore()
|
|
|
|
// État
|
|
const konamiIndex = ref(0)
|
|
const clickSequence = ref<string[]>([])
|
|
|
|
// Charger les easter eggs au montage
|
|
onMounted(() => {
|
|
fetchList()
|
|
initKonamiListener()
|
|
})
|
|
|
|
// === Konami Code ===
|
|
function initKonamiListener() {
|
|
window.addEventListener('keydown', handleKonamiKey)
|
|
}
|
|
|
|
function handleKonamiKey(e: KeyboardEvent) {
|
|
if (e.code === KONAMI_CODE[konamiIndex.value]) {
|
|
konamiIndex.value++
|
|
if (konamiIndex.value === KONAMI_CODE.length) {
|
|
triggerEasterEgg('konami-master')
|
|
konamiIndex.value = 0
|
|
}
|
|
} else {
|
|
konamiIndex.value = 0
|
|
}
|
|
}
|
|
|
|
// === Click Detection ===
|
|
function detectClick(elementId: string, targetSlug: string, requiredClicks: number = 1) {
|
|
const clicks = ref(0)
|
|
|
|
function handleClick() {
|
|
clicks.value++
|
|
if (clicks.value >= requiredClicks) {
|
|
triggerEasterEgg(targetSlug)
|
|
clicks.value = 0
|
|
}
|
|
}
|
|
|
|
return { handleClick, clicks }
|
|
}
|
|
|
|
// === Hover Detection ===
|
|
function detectHover(targetSlug: string, hoverTime: number = 2000) {
|
|
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
|
|
function handleMouseEnter() {
|
|
timeoutId = setTimeout(() => {
|
|
triggerEasterEgg(targetSlug)
|
|
}, hoverTime)
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId)
|
|
timeoutId = null
|
|
}
|
|
}
|
|
|
|
return { handleMouseEnter, handleMouseLeave }
|
|
}
|
|
|
|
// === Scroll Detection ===
|
|
function detectScrollBottom(targetSlug: string) {
|
|
function checkScroll() {
|
|
const scrollTop = window.scrollY
|
|
const windowHeight = window.innerHeight
|
|
const docHeight = document.documentElement.scrollHeight
|
|
|
|
if (scrollTop + windowHeight >= docHeight - 50) {
|
|
triggerEasterEgg(targetSlug)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('scroll', checkScroll, { passive: true })
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('scroll', checkScroll)
|
|
})
|
|
}
|
|
|
|
// === Sequence Detection ===
|
|
function detectSequence(expectedSequence: string[], targetSlug: string) {
|
|
function addToSequence(item: string) {
|
|
clickSequence.value.push(item)
|
|
|
|
// Garder seulement les N derniers
|
|
if (clickSequence.value.length > expectedSequence.length) {
|
|
clickSequence.value.shift()
|
|
}
|
|
|
|
// Vérifier si la séquence correspond
|
|
if (clickSequence.value.length === expectedSequence.length) {
|
|
const match = clickSequence.value.every((val, idx) => val === expectedSequence[idx])
|
|
if (match) {
|
|
triggerEasterEgg(targetSlug)
|
|
clickSequence.value = []
|
|
}
|
|
}
|
|
}
|
|
|
|
return { addToSequence }
|
|
}
|
|
|
|
// === Trigger Easter Egg ===
|
|
async function triggerEasterEgg(slug: string) {
|
|
// Vérifier si déjà trouvé
|
|
if (progressionStore.easterEggsFound.includes(slug)) {
|
|
return
|
|
}
|
|
|
|
// Marquer comme trouvé
|
|
progressionStore.markEasterEggFound(slug)
|
|
|
|
// Notifier
|
|
options.onFound(slug)
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKonamiKey)
|
|
})
|
|
|
|
return {
|
|
detectClick,
|
|
detectHover,
|
|
detectScrollBottom,
|
|
detectSequence,
|
|
triggerEasterEgg,
|
|
}
|
|
}
|
|
```
|
|
|
|
### Composant EasterEggPopup
|
|
|
|
```vue
|
|
<!-- frontend/app/components/feature/EasterEggPopup.vue -->
|
|
<script setup lang="ts">
|
|
interface EasterEggReward {
|
|
slug: string
|
|
reward_type: 'snippet' | 'anecdote' | 'image' | 'badge'
|
|
reward: string
|
|
difficulty: number
|
|
}
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
reward: EasterEggReward | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
close: []
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const progressionStore = useProgressionStore()
|
|
const { availableEasterEggs } = useFetchEasterEggs()
|
|
|
|
const totalEasterEggs = computed(() => availableEasterEggs.value.length || 8)
|
|
const foundCount = computed(() => progressionStore.easterEggsFoundCount)
|
|
|
|
// Icône selon le type
|
|
const rewardIcon = computed(() => {
|
|
if (!props.reward) return '🎁'
|
|
const icons: Record<string, string> = {
|
|
snippet: '💻',
|
|
anecdote: '📖',
|
|
image: '🖼️',
|
|
badge: '🏆',
|
|
}
|
|
return icons[props.reward.reward_type] || '🎁'
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition name="popup">
|
|
<div
|
|
v-if="visible && reward"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
>
|
|
<!-- Overlay -->
|
|
<div
|
|
class="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
|
@click="emit('close')"
|
|
></div>
|
|
|
|
<!-- Modal -->
|
|
<div class="relative bg-sky-dark-50 rounded-2xl p-8 max-w-md w-full border border-sky-accent/50 shadow-2xl shadow-sky-accent/20 animate-bounce-in">
|
|
<!-- Effet confetti/sparkles -->
|
|
<div class="absolute -top-4 left-1/2 -translate-x-1/2 text-4xl animate-bounce">
|
|
🎉
|
|
</div>
|
|
|
|
<!-- Icône du type -->
|
|
<div class="text-6xl text-center mb-4">
|
|
{{ rewardIcon }}
|
|
</div>
|
|
|
|
<!-- Titre -->
|
|
<h2 class="text-2xl font-ui font-bold text-sky-accent text-center mb-2">
|
|
{{ t('easterEgg.found') }}
|
|
</h2>
|
|
|
|
<!-- Compteur -->
|
|
<p class="text-sm text-sky-text-muted text-center mb-6">
|
|
{{ t('easterEgg.count', { found: foundCount, total: totalEasterEggs }) }}
|
|
</p>
|
|
|
|
<!-- Récompense -->
|
|
<div class="bg-sky-dark rounded-lg p-4 mb-6">
|
|
<!-- Snippet de code -->
|
|
<pre
|
|
v-if="reward.reward_type === 'snippet'"
|
|
class="font-mono text-sm text-sky-accent overflow-x-auto"
|
|
><code>{{ reward.reward }}</code></pre>
|
|
|
|
<!-- Anecdote ou texte -->
|
|
<p
|
|
v-else-if="reward.reward_type === 'anecdote'"
|
|
class="font-narrative text-sky-text italic"
|
|
>
|
|
{{ reward.reward }}
|
|
</p>
|
|
|
|
<!-- Badge -->
|
|
<div
|
|
v-else-if="reward.reward_type === 'badge'"
|
|
class="text-center"
|
|
>
|
|
<p class="font-ui text-sky-text">{{ reward.reward }}</p>
|
|
</div>
|
|
|
|
<!-- Image -->
|
|
<div
|
|
v-else-if="reward.reward_type === 'image'"
|
|
class="text-center"
|
|
>
|
|
<p class="font-ui text-sky-text">{{ reward.reward }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Difficulté -->
|
|
<div class="flex items-center justify-center gap-1 mb-6">
|
|
<span class="text-xs text-sky-text-muted mr-2">{{ t('easterEgg.difficulty') }}:</span>
|
|
<span
|
|
v-for="i in 5"
|
|
:key="i"
|
|
class="text-sm"
|
|
:class="i <= reward.difficulty ? 'text-sky-accent' : 'text-sky-dark-100'"
|
|
>
|
|
⭐
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Bouton fermer -->
|
|
<button
|
|
type="button"
|
|
class="w-full py-3 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
|
|
@click="emit('close')"
|
|
>
|
|
{{ t('common.continue') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.popup-enter-active,
|
|
.popup-leave-active {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.popup-enter-from,
|
|
.popup-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.popup-enter-from .relative,
|
|
.popup-leave-to .relative {
|
|
transform: scale(0.9);
|
|
}
|
|
|
|
@keyframes bounce-in {
|
|
0% {
|
|
transform: scale(0.5);
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
transform: scale(1.05);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.animate-bounce-in {
|
|
animation: bounce-in 0.4s ease-out;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### Composant EasterEggCollection
|
|
|
|
```vue
|
|
<!-- frontend/app/components/feature/EasterEggCollection.vue -->
|
|
<script setup lang="ts">
|
|
const { t } = useI18n()
|
|
const progressionStore = useProgressionStore()
|
|
const { availableEasterEggs, fetchList } = useFetchEasterEggs()
|
|
|
|
onMounted(() => {
|
|
fetchList()
|
|
})
|
|
|
|
const totalEasterEggs = computed(() => availableEasterEggs.value.length || 8)
|
|
const foundCount = computed(() => progressionStore.easterEggsFoundCount)
|
|
const isComplete = computed(() => foundCount.value >= totalEasterEggs.value)
|
|
|
|
function isFound(slug: string): boolean {
|
|
return progressionStore.easterEggsFound.includes(slug)
|
|
}
|
|
|
|
// Icône selon difficulté
|
|
function getDifficultyStars(difficulty: number): string {
|
|
return '⭐'.repeat(difficulty) + '☆'.repeat(5 - difficulty)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="easter-egg-collection">
|
|
<!-- Header avec compteur -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-ui font-bold text-sky-text">
|
|
{{ t('easterEgg.collection') }}
|
|
</h2>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sky-accent font-ui font-bold">{{ foundCount }}</span>
|
|
<span class="text-sky-text-muted">/</span>
|
|
<span class="text-sky-text-muted">{{ totalEasterEggs }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Badge 100% -->
|
|
<div
|
|
v-if="isComplete"
|
|
class="bg-gradient-to-r from-sky-accent to-amber-500 rounded-lg p-4 mb-6 text-center"
|
|
>
|
|
<span class="text-2xl">🏆</span>
|
|
<p class="text-white font-ui font-bold mt-2">
|
|
{{ t('easterEgg.allFound') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Barre de progression -->
|
|
<div class="h-2 bg-sky-dark-100 rounded-full mb-6 overflow-hidden">
|
|
<div
|
|
class="h-full bg-sky-accent transition-all duration-500"
|
|
:style="{ width: `${(foundCount / totalEasterEggs) * 100}%` }"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Grille des easter eggs -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div
|
|
v-for="egg in availableEasterEggs"
|
|
:key="egg.slug"
|
|
class="easter-egg-card p-4 rounded-lg border transition-all"
|
|
:class="[
|
|
isFound(egg.slug)
|
|
? 'bg-sky-dark-50 border-sky-accent/50'
|
|
: 'bg-sky-dark border-sky-dark-100 opacity-50'
|
|
]"
|
|
>
|
|
<!-- Icône ou mystère -->
|
|
<div class="text-3xl text-center mb-2">
|
|
{{ isFound(egg.slug) ? getTriggerIcon(egg.trigger_type) : '❓' }}
|
|
</div>
|
|
|
|
<!-- Nom ou mystère -->
|
|
<p class="text-sm font-ui text-center truncate" :class="isFound(egg.slug) ? 'text-sky-text' : 'text-sky-text-muted'">
|
|
{{ isFound(egg.slug) ? formatSlug(egg.slug) : '???' }}
|
|
</p>
|
|
|
|
<!-- Difficulté -->
|
|
<p class="text-xs text-center mt-1 text-sky-text-muted">
|
|
{{ getDifficultyStars(egg.difficulty) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Indice si pas tous trouvés -->
|
|
<p
|
|
v-if="!isComplete"
|
|
class="text-sm text-sky-text-muted text-center mt-6 font-narrative italic"
|
|
>
|
|
{{ t('easterEgg.hint') }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
function getTriggerIcon(trigger: string): string {
|
|
const icons: Record<string, string> = {
|
|
click: '👆',
|
|
hover: '👀',
|
|
konami: '🎮',
|
|
scroll: '📜',
|
|
sequence: '🔢',
|
|
}
|
|
return icons[trigger] || '🎁'
|
|
}
|
|
|
|
function formatSlug(slug: string): string {
|
|
return slug
|
|
.split('-')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Clés i18n
|
|
|
|
**fr.json :**
|
|
```json
|
|
{
|
|
"easterEgg": {
|
|
"found": "Easter Egg trouvé !",
|
|
"count": "{found} / {total} découverts",
|
|
"difficulty": "Difficulté",
|
|
"collection": "Ma Collection",
|
|
"allFound": "Collection complète ! Tu es un vrai explorateur !",
|
|
"hint": "Continue d'explorer... des surprises sont cachées partout !"
|
|
}
|
|
}
|
|
```
|
|
|
|
**en.json :**
|
|
```json
|
|
{
|
|
"easterEgg": {
|
|
"found": "Easter Egg found!",
|
|
"count": "{found} / {total} discovered",
|
|
"difficulty": "Difficulty",
|
|
"collection": "My Collection",
|
|
"allFound": "Collection complete! You're a true explorer!",
|
|
"hint": "Keep exploring... surprises are hidden everywhere!"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Intégration dans une page (exemple)
|
|
|
|
```vue
|
|
<!-- frontend/app/pages/projets.vue (extrait) -->
|
|
<script setup>
|
|
const showEasterEggPopup = ref(false)
|
|
const currentReward = ref(null)
|
|
|
|
const { validate } = useFetchEasterEggs()
|
|
const narrator = useNarrator()
|
|
|
|
const { detectHover } = useEasterEggDetection({
|
|
onFound: async (slug) => {
|
|
const reward = await validate(slug)
|
|
if (reward) {
|
|
currentReward.value = reward
|
|
showEasterEggPopup.value = true
|
|
narrator.showEasterEggFound()
|
|
}
|
|
}
|
|
})
|
|
|
|
// Hover sur le commentaire secret
|
|
const secretCommentHover = detectHover('secret-comment', 2000)
|
|
</script>
|
|
|
|
<template>
|
|
<!-- ... contenu de la page ... -->
|
|
|
|
<!-- Élément avec easter egg hover -->
|
|
<span
|
|
class="cursor-help"
|
|
@mouseenter="secretCommentHover.handleMouseEnter"
|
|
@mouseleave="secretCommentHover.handleMouseLeave"
|
|
>
|
|
/* ... */
|
|
</span>
|
|
|
|
<!-- Popup easter egg -->
|
|
<EasterEggPopup
|
|
:visible="showEasterEggPopup"
|
|
:reward="currentReward"
|
|
@close="showEasterEggPopup = false"
|
|
/>
|
|
</template>
|
|
```
|
|
|
|
### Dépendances
|
|
|
|
**Cette story nécessite :**
|
|
- Story 4.4 : API et store des easter eggs
|
|
- Story 3.3 : useNarrator (réaction du narrateur)
|
|
|
|
**Cette story prépare pour :**
|
|
- Story 4.8 : Page contact (statistiques de collection)
|
|
|
|
### Project Structure Notes
|
|
|
|
**Fichiers à créer :**
|
|
```
|
|
frontend/app/
|
|
├── composables/
|
|
│ └── useEasterEggDetection.ts # CRÉER
|
|
└── components/feature/
|
|
├── EasterEggPopup.vue # CRÉER
|
|
├── EasterEggNotification.vue # CRÉER
|
|
└── EasterEggCollection.vue # CRÉER
|
|
```
|
|
|
|
**Fichiers à modifier :**
|
|
```
|
|
frontend/app/pages/projets.vue # AJOUTER détecteurs
|
|
frontend/app/pages/parcours.vue # AJOUTER détecteurs
|
|
frontend/app/pages/competences.vue # AJOUTER détecteurs
|
|
frontend/app/components/layout/AppHeader.vue # AJOUTER araignée cachée
|
|
frontend/app/components/feature/SettingsDrawer.vue # AJOUTER collection
|
|
frontend/i18n/fr.json # AJOUTER easterEgg.*
|
|
frontend/i18n/en.json # AJOUTER easterEgg.*
|
|
```
|
|
|
|
### References
|
|
|
|
- [Source: docs/planning-artifacts/epics.md#Story-4.5]
|
|
- [Source: docs/planning-artifacts/ux-design-specification.md#Easter-Eggs-UI]
|
|
- [Source: docs/brainstorming-gamification-2026-01-26.md#Easter-Eggs]
|
|
|
|
### Technical Requirements
|
|
|
|
| Requirement | Value | Source |
|
|
|-------------|-------|--------|
|
|
| Types de triggers | click, hover, konami, scroll, sequence | Epics |
|
|
| Types de récompenses | snippet, anecdote, image, badge | Epics |
|
|
| Collection | Grille avec mystères | Epics |
|
|
| Badge 100% | Affiché si complet | Epics |
|
|
|
|
## 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
|
|
|