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
requiresAuthsi déloggué - Bloque
requiresAdminsi non-admin - Redirige
/pendingsiuser.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éeinvalidate()force reload (après ajout/suppression)
stores/session.ts
- Persistence localStorage 6h (
STORAGE_KEY = 'vellum.session.v1') activeGame + activeExtensions + messages[]aveccitations[],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
- Scoped vs Tailwind hidden : un
display: flexen CSS scoped écrase lehidden lg:flexdu template. Préférer Tailwind utilities partout. vue-tsc --noEmit: passe parfois en local mais foire en CI (vue-tsc --build). Tester avecnpm run build.- Unicode / accents dans card names : utiliser
normalizeCardKey()(useArbiterMarkdown.ts:74-81) — NFC + lowercase + apostrophe/tiret normalization. Évite les bugs sur Lorcana / Magic japonais. \b(word boundary) sans flagu: ne reconnaît que[A-Za-z0-9_]. Un nom finissant paréne match pas. Utiliser un lookahead Unicode-aware avec flagu:(?=\\s|$|[^\\p{L}\\p{N}_]).- Modales async :
CardZoomModal+DeckImportModalchargés viadefineAsyncComponent()— gain ~50 KB gzip sur le bundle initial PlayView.
No comments to display
No comments to display