Skip to main content

Architecture — Frontend

Architecture — Frontend

Dernière mise à jour : 2026-05-10

Structure

frontend/src/
├── views/             # 15 pages routables (LoginView, PlayView, AdminView, etc.)
├── components/        # ~50 composants Vue, par domaine
│   ├── admin/         # AdminGamesSection, AdminUsersSection, AdminCardDecksSection, AdminFeedbackDetail
│   ├── add-game/      # Wizard 3 étapes (BGG search → upload → ingest ritual)
│   ├── play/          # PlayComposer, PlayChatMessages, PlayBackground, PlayHeader
│   ├── deck-import/   # DeckImportForm, DeckImportPreview
│   ├── card-zoom/     # CardZoomStats (modal zoom cartes)
│   ├── home/          # HomeGameGrid, HomeResumeBanner
│   └── (standalone)   # ChatMessage, ArbiterResponse, CardPreview, NavSidebar, etc.
├── composables/       # ~14 composables (useAskStream, useMentionAutocomplete, useArbiterMarkdown, usePlaySession…)
├── stores/            # Pinia (auth, games, session)
├── services/          # `api.ts` — couche client unique vers backend
├── lib/               # Utilitaires : fab-symbols.ts, mana.ts, riftbound-symbols.ts, lorcana-symbols.ts
├── assets/            # CSS tokens : tokens.css, main.css, motion-tokens.css, TCG-specific (fab.css, lorcana.css, riftbound.css)
└── router/index.ts    # Routes + guards auth/admin

Routeur

router/index.ts (78 lignes). router.beforeEach :

  • Bloque routes requiresAuth si déloggué
  • Bloque requiresAdmin si non-admin
  • Redirige /pending si user.role === 'pending'
Route Composant Auth Admin
/login LoginView N N
/register RegisterView N N
/confirm-success ConfirmSuccessView N N
/reset-password ResetPasswordView N N
/pending PendingView Y N
/ HomeView Y N
/play PlayView Y N
/history HistoryView Y N
/add-game AddGameView Y N
/ingest/:id IngestView Y N
/me AccountView Y N
/me/settings SettingsView Y N
/admin AdminView Y Y
/admin/feedback AdminFeedbackView Y Y

State management — Pinia stores

stores/auth.ts

  • user: { id, username, role, canAddGames } | null
  • Computed : isLoggedIn, isAdmin, isPending, canAddGames
  • Actions : checkSession(), login(), register(), logout()
  • Auth via cookie HTTP-only — credentials: 'include' dans tous les fetch

stores/games.ts

  • Cache TTL 60s (collator fr-FR numeric pour tri)
  • Dedup appels concurrents (promise inflight)
  • fetch() retourne la liste triée
  • invalidate() force reload (après ajout/suppression)

stores/session.ts

  • Persistence localStorage 6h (STORAGE_KEY = 'vellum.session.v1')
  • activeGame + activeExtensions + messages[] avec citations[], cardMentions[], mentionedCards[], synergyCards[]
  • Watch deep avec throttle 200ms localStorage

Composables clés

Composable Rôle
useAskStream Wrapper SSE /api/ask/stream + fallback polling 3s × 15 si SSE casse
useEventStream Bas niveau : fetch streaming + parsing SSE générique (filtre les heartbeats)
useMentionAutocomplete Popover @-cartes : debounce 150ms, nav clavier, sync mentions au submit
useArbiterMarkdown marked.parse + injection citations + tokens TCG (mana, FAB, Riftbound, Lorcana)
usePlaySession Orchestre PlayView : SSE, table mode, sticky mentions, deck attachment, card lookup
useTableMode Toggle localStorage 'table-mode'
useDeckAttachment AttachedDeck { deckName, format, hero, cards[] }
useCardLookup Map cardKey → { id, name, imageUrl, orientation }
useStickyMentions buildStickyMentions() : extrait + dédoublonne, FIFO 20 (80 si deck)
useDeckImport parseDeck(gameId) : POST /api/decks/parse

Service api.ts (351 lignes)

Couche client unique pour tous les appels backend. Centralise :

  • credentials: 'include' (cookies)
  • 401 → redirect /login (sauf sur /login, /register, /reset-password)
  • erreur → throw Error(body.error)
  • OK → return res.json()

Types exportés : Game, CardSearchResult, MentionedCard, DeckParseResponse, etc.

Design tokens

frontend/src/assets/tokens.css (palette OKLCH dark-first) :

  • Bois --ref-wood-950 à 600 (table sombre, grain chaud)
  • Feutre --ref-felt-950 à 600 (tapis vert)
  • Parchemin --ref-parchment-50 à 500 (ivoire chaud)
  • Or --ref-gold-300 à 700 (cuivre signature)
  • Meeple --ref-meeple-400 à 600 (vert CTA alternatif)
  • Dé/Sang --ref-dice-400 à 600 (rouge erreur)

Sémantiques (main.css @theme) :

  • --color-bg, --color-bg-felt, --color-ink, --color-primary, --color-citation-bg, --color-citation-bar

Build & dev

Commande Effet
npm run dev Vite dev :5173 + proxy /api → :3000
npm run build type-check (vue-tsc --build) + vite build (en parallèle)
npm run build-only vite build seul (dist/)
npm run type-check vue-tsc --build (pas --noEmit : compile full)
npm test / npm run test:watch Vitest

Vite config : alias @src/, plugin @tailwindcss/vite, proxy /api → :3000.

Pièges connus

  1. Scoped vs Tailwind hidden : un display: flex en CSS scoped écrase le hidden lg:flex du template. Préférer Tailwind utilities partout.
  2. vue-tsc --noEmit : passe parfois en local mais foire en CI (vue-tsc --build). Tester avec npm run build.
  3. Unicode / accents dans card names : utiliser normalizeCardKey() (useArbiterMarkdown.ts:74-81) — NFC + lowercase + apostrophe/tiret normalization. Évite les bugs sur Lorcana / Magic japonais.
  4. \b (word boundary) sans flag u : ne reconnaît que [A-Za-z0-9_]. Un nom finissant par é ne match pas. Utiliser un lookahead Unicode-aware avec flag u : (?=\\s|$|[^\\p{L}\\p{N}_]).
  5. Modales async : CardZoomModal + DeckImportModal chargés via defineAsyncComponent() — gain ~50 KB gzip sur le bundle initial PlayView.