Files
Portfolio-Game/docs/implementation-artifacts/3-4-barre-progression-globale-xp-bar.md
skycel ec1ae92799 🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
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>
2026-02-05 02:08:56 +01:00

15 KiB

Story 3.4: Barre de progression globale (XP bar)

Status: ready-for-dev

Story

As a visiteur, I want voir ma progression dans l'exploration du site, so that je sais combien il me reste à découvrir.

Acceptance Criteria

  1. Given le visiteur est en mode Aventure When il navigue sur le site Then une barre de progression discrète s'affiche dans le header
  2. And le pourcentage est calculé selon les sections visitées (Projets, Compétences, Témoignages, Parcours)
  3. And l'animation de la barre est fluide lors des mises à jour
  4. And un tooltip au hover indique les sections visitées et restantes
  5. And le design évoque une barre XP style RPG (cohérent avec sky-accent)
  6. And la barre respecte prefers-reduced-motion (pas d'animation si activé)
  7. And sur mobile, la progression est accessible via la bottom bar
  8. And la barre n'est pas visible en mode Express/Résumé

Tasks / Subtasks

  • Task 1: Créer le composant ProgressBar (AC: #1, #3, #5, #6)

    • Créer frontend/app/components/feature/ProgressBar.vue
    • Props : percent (number), showTooltip (boolean)
    • Design XP bar style RPG avec sky-accent
    • Animation fluide de remplissage (CSS transition)
    • Respecter prefers-reduced-motion
  • Task 2: Implémenter le tooltip des sections (AC: #4)

    • Afficher au hover la liste des sections
    • Indiquer le statut : visitée (✓) ou à découvrir
    • Utiliser Headless UI Popover ou tooltip custom
    • Traductions FR/EN
  • Task 3: Intégrer dans le header (AC: #1, #8)

    • Ajouter la ProgressBar dans le composant Header
    • Conditionner l'affichage : visible uniquement en mode Aventure
    • Masquer si expressMode === true dans le store
    • Position : à droite du header, avant le language switcher
  • Task 4: Calculer le pourcentage (AC: #2)

    • Définir les 4 sections : projets, competences, temoignages, parcours
    • Chaque section visitée = 25%
    • Lire depuis visitedSections du store
    • Le calcul est fait dans le store (completionPercent)
  • Task 5: Version mobile (AC: #7)

    • Sur mobile, la barre est masquée du header
    • La progression est accessible via l'icône dans la bottom bar
    • Un tap affiche un mini-modal ou drawer avec le détail
  • Task 6: Effets visuels RPG (AC: #5)

    • Effet de brillance/glow au survol
    • Particules optionnelles quand la barre augmente
    • Bordure stylisée évoquant un cadre de jeu
    • Graduation subtile sur la barre
  • Task 7: Tests et validation

    • Tester l'animation de remplissage
    • Vérifier le tooltip (desktop)
    • Valider la version mobile (bottom bar)
    • Tester prefers-reduced-motion
    • Vérifier que la barre est masquée en mode Express

Dev Notes

Composant ProgressBar

<!-- frontend/app/components/feature/ProgressBar.vue -->
<script setup lang="ts">
const props = withDefaults(defineProps<{
  percent: number
  showTooltip?: boolean
  compact?: boolean // Pour la version mobile
}>(), {
  showTooltip: true,
  compact: false,
})

const { t } = useI18n()
const progressionStore = useProgressionStore()
const reducedMotion = useReducedMotion()

// Sections avec leur statut
const sections = computed(() => [
  {
    key: 'projets',
    name: t('progress.sections.projects'),
    visited: progressionStore.visitedSections.includes('projets'),
  },
  {
    key: 'competences',
    name: t('progress.sections.skills'),
    visited: progressionStore.visitedSections.includes('competences'),
  },
  {
    key: 'temoignages',
    name: t('progress.sections.testimonials'),
    visited: progressionStore.visitedSections.includes('temoignages'),
  },
  {
    key: 'parcours',
    name: t('progress.sections.journey'),
    visited: progressionStore.visitedSections.includes('parcours'),
  },
])

const visitedCount = computed(() => sections.value.filter(s => s.visited).length)
const remainingCount = computed(() => 4 - visitedCount.value)

// État du tooltip
const showPopover = ref(false)
</script>

<template>
  <div
    class="progress-bar-container relative"
    :class="{ 'compact': compact }"
  >
    <!-- Barre principale -->
    <div
      class="progress-bar-wrapper group cursor-pointer"
      @mouseenter="showPopover = true"
      @mouseleave="showPopover = false"
      @focus="showPopover = true"
      @blur="showPopover = false"
      tabindex="0"
      role="progressbar"
      :aria-valuenow="percent"
      aria-valuemin="0"
      aria-valuemax="100"
      :aria-label="t('progress.label', { percent })"
    >
      <!-- Cadre RPG -->
      <div class="progress-frame relative h-6 w-40 rounded-full border-2 border-sky-accent/50 bg-sky-dark overflow-hidden">
        <!-- Fond avec graduation -->
        <div class="absolute inset-0 flex">
          <div
            v-for="i in 4"
            :key="i"
            class="flex-1 border-r border-sky-dark-100/30 last:border-r-0"
          ></div>
        </div>

        <!-- Barre de remplissage -->
        <div
          class="progress-fill absolute inset-y-0 left-0 bg-gradient-to-r from-sky-accent to-sky-accent-light"
          :class="{ 'transition-all duration-500 ease-out': !reducedMotion }"
          :style="{ width: `${percent}%` }"
        >
          <!-- Effet de brillance -->
          <div class="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent"></div>

          <!-- Effet glow au survol -->
          <div
            class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity"
            :class="{ 'transition-none': reducedMotion }"
            style="box-shadow: 0 0 10px var(--sky-accent), 0 0 20px var(--sky-accent)"
          ></div>
        </div>

        <!-- Pourcentage -->
        <div class="absolute inset-0 flex items-center justify-center">
          <span class="text-xs font-ui font-bold text-sky-text drop-shadow-md">
            {{ percent }}%
          </span>
        </div>
      </div>
    </div>

    <!-- Tooltip -->
    <Transition name="fade">
      <div
        v-if="showTooltip && showPopover"
        class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50"
      >
        <div class="bg-sky-dark-50 border border-sky-dark-100 rounded-lg shadow-xl p-3 min-w-48">
          <!-- Titre -->
          <p class="text-sm font-ui font-semibold text-sky-text mb-2">
            {{ t('progress.title') }}
          </p>

          <!-- Liste des sections -->
          <ul class="space-y-1">
            <li
              v-for="section in sections"
              :key="section.key"
              class="flex items-center gap-2 text-sm"
            >
              <span
                v-if="section.visited"
                class="text-green-400"
              ></span>
              <span
                v-else
                class="text-sky-text-muted"
              ></span>
              <span
                :class="section.visited ? 'text-sky-text' : 'text-sky-text-muted'"
              >
                {{ section.name }}
              </span>
            </li>
          </ul>

          <!-- Résumé -->
          <p class="text-xs text-sky-text-muted mt-2 pt-2 border-t border-sky-dark-100">
            {{ t('progress.summary', { visited: visitedCount, remaining: remainingCount }) }}
          </p>

          <!-- Flèche du tooltip -->
          <div class="absolute -top-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-sky-dark-50 border-l border-t border-sky-dark-100 transform rotate-45"></div>
        </div>
      </div>
    </Transition>
  </div>
</template>

<style scoped>
.progress-bar-wrapper:focus {
  outline: 2px solid var(--sky-accent);
  outline-offset: 2px;
  border-radius: 9999px;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.15s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* Version compacte pour mobile */
.compact .progress-frame {
  height: 1rem;
  width: 100%;
}

.compact .progress-frame span {
  font-size: 0.65rem;
}

@media (prefers-reduced-motion: reduce) {
  .progress-fill {
    transition: none;
  }
}
</style>

Clés i18n

fr.json :

{
  "progress": {
    "label": "Progression : {percent}%",
    "title": "Exploration du portfolio",
    "sections": {
      "projects": "Projets",
      "skills": "Compétences",
      "testimonials": "Témoignages",
      "journey": "Parcours"
    },
    "summary": "{visited} visité(s), {remaining} à découvrir"
  }
}

en.json :

{
  "progress": {
    "label": "Progress: {percent}%",
    "title": "Portfolio exploration",
    "sections": {
      "projects": "Projects",
      "skills": "Skills",
      "testimonials": "Testimonials",
      "journey": "Journey"
    },
    "summary": "{visited} visited, {remaining} to discover"
  }
}

Intégration dans le Header

<!-- frontend/app/components/layout/AppHeader.vue (extrait) -->
<script setup>
const progressionStore = useProgressionStore()

// Afficher la barre uniquement en mode Aventure (pas en Express)
const showProgressBar = computed(() => {
  return !progressionStore.expressMode
})
</script>

<template>
  <header class="app-header">
    <!-- ... autres éléments ... -->

    <!-- Progress Bar (desktop only, mode Aventure) -->
    <ProgressBar
      v-if="showProgressBar"
      :percent="progressionStore.completionPercent"
      class="hidden md:block"
    />

    <!-- Language Switcher -->
    <!-- ... -->
  </header>
</template>

Composant ProgressIcon pour mobile (Bottom Bar)

<!-- frontend/app/components/feature/ProgressIcon.vue -->
<script setup lang="ts">
const progressionStore = useProgressionStore()
const { t } = useI18n()

const showDrawer = ref(false)
</script>

<template>
  <button
    type="button"
    class="progress-icon relative p-3"
    :aria-label="t('progress.label', { percent: progressionStore.completionPercent })"
    @click="showDrawer = true"
  >
    <!-- Icône avec indicateur circulaire -->
    <div class="relative w-8 h-8">
      <!-- Cercle de progression -->
      <svg class="w-full h-full -rotate-90" viewBox="0 0 36 36">
        <!-- Fond -->
        <circle
          cx="18"
          cy="18"
          r="16"
          fill="none"
          stroke="currentColor"
          stroke-width="3"
          class="text-sky-dark-100"
        />
        <!-- Progression -->
        <circle
          cx="18"
          cy="18"
          r="16"
          fill="none"
          stroke="currentColor"
          stroke-width="3"
          stroke-linecap="round"
          class="text-sky-accent"
          :stroke-dasharray="`${progressionStore.completionPercent}, 100`"
        />
      </svg>
      <!-- Pourcentage au centre -->
      <span class="absolute inset-0 flex items-center justify-center text-xs font-ui font-bold text-sky-text">
        {{ progressionStore.completionPercent }}
      </span>
    </div>
  </button>

  <!-- Drawer/Modal avec détail -->
  <Teleport to="body">
    <Transition name="slide-up">
      <div
        v-if="showDrawer"
        class="fixed inset-x-0 bottom-0 z-50 bg-sky-dark-50 rounded-t-2xl shadow-xl p-4 pb-safe"
        style="bottom: var(--bottom-bar-height, 64px)"
      >
        <!-- Handle -->
        <div class="w-12 h-1 bg-sky-dark-100 rounded-full mx-auto mb-4"></div>

        <!-- Contenu -->
        <h3 class="text-lg font-ui font-bold text-sky-text mb-4">
          {{ t('progress.title') }}
        </h3>

        <!-- Barre compacte -->
        <ProgressBar
          :percent="progressionStore.completionPercent"
          :show-tooltip="false"
          compact
          class="mb-4"
        />

        <!-- Liste des sections -->
        <!-- ... même logique que le tooltip desktop ... -->

        <!-- Bouton fermer -->
        <button
          type="button"
          class="mt-4 w-full py-2 bg-sky-dark-100 rounded-lg text-sky-text font-ui"
          @click="showDrawer = false"
        >
          {{ t('common.close') }}
        </button>
      </div>
    </Transition>

    <!-- Overlay -->
    <Transition name="fade">
      <div
        v-if="showDrawer"
        class="fixed inset-0 bg-black/50 z-40"
        @click="showDrawer = false"
      ></div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.slide-up-enter-active,
.slide-up-leave-active {
  transition: transform 0.3s ease;
}

.slide-up-enter-from,
.slide-up-leave-to {
  transform: translateY(100%);
}
</style>

Calcul du pourcentage dans le store

// frontend/app/stores/progression.ts (extrait)

// Sections disponibles pour la progression
const AVAILABLE_SECTIONS = ['projets', 'competences', 'temoignages', 'parcours'] as const
type Section = typeof AVAILABLE_SECTIONS[number]

export const useProgressionStore = defineStore('progression', () => {
  const visitedSections = ref<Section[]>([])

  const completionPercent = computed(() => {
    const visitedCount = visitedSections.value.length
    return Math.round((visitedCount / AVAILABLE_SECTIONS.length) * 100)
  })

  function visitSection(section: Section) {
    if (!visitedSections.value.includes(section)) {
      visitedSections.value.push(section)
    }
  }

  return {
    visitedSections,
    completionPercent,
    visitSection,
  }
})

Dépendances

Cette story nécessite :

  • Story 1.6 : Store Pinia (visitedSections, completionPercent, expressMode)
  • Story 3.2 : useReducedMotion composable

Cette story prépare pour :

  • Story 3.5 : Logique de progression (complète le store)
  • Story 3.7 : Navigation mobile (utilise ProgressIcon)

Project Structure Notes

Fichiers à créer :

frontend/app/components/feature/
├── ProgressBar.vue                      # CRÉER
└── ProgressIcon.vue                     # CRÉER (version mobile)

Fichiers à modifier :

frontend/app/components/layout/AppHeader.vue  # AJOUTER ProgressBar
frontend/i18n/fr.json                         # AJOUTER progress.*
frontend/i18n/en.json                         # AJOUTER progress.*

References

  • [Source: docs/planning-artifacts/epics.md#Story-3.4]
  • [Source: docs/planning-artifacts/ux-design-specification.md#XP-Bar]
  • [Source: docs/planning-artifacts/ux-design-specification.md#Design-Tokens]

Technical Requirements

Requirement Value Source
Sections 4 (25% chacune) Epics
Couleur sky-accent (#fa784f) UX Spec
Animation CSS transition 500ms Décision technique
Position desktop Header, à droite Epics
Position mobile Bottom bar (icône) 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