EmberAnvil — Vue 3 + Vite web application (virtual forge/crafting game). All UI text and most variable names/comments are in French.
- Framework: Vue 3 (Options API — never Composition API or
<script setup>) - Build: Vite 8.x | Styling: Tailwind CSS 4.x | Router: Vue Router 5.x
- State: Pinia 3.x with
pinia-plugin-persistedstate(configured globally insrc/stores/index.js) - Icons:
lucide-vue-next— import per-icon:import { Hammer } from "@lucide/vue" - Language: JavaScript ES modules — no TypeScript, no
.tsfiles - Deployment: Vercel (
vercel.jsonat root)
npm run dev # Dev server (Vite)
npm run build # Production build
npm run build:sitemap # Build + generate sitemap
npm run preview # Preview production build
npm run lint # Lint + auto-fix (oxlint)
npm run format # Format with Prettier (@prettier/plugin-oxc)
# Testing — Vitest + jsdom + @vue/test-utils
npm run test:run # Single full run (use this to verify changes)
npm test # Watch mode
npm run test:coverage # Coverage report
# Run a single test file
npx vitest run src/__tests__/stores/workshop.spec.js
npx vitest run src/__tests__/ui/PageMain.spec.js
# Run tests matching a name pattern
npx vitest run --reporter=verbose -t "should toggle FAQ"Always run npm run lint and npm run test:run after any code change.
src/
├── domains/ # blog/ crafting/ inventory/ player/ shop/ wiki/ workshop/
│ └── <domain>/ # components/ + views/ (+ __tests__/ for shop, workshop)
├── stores/ # one .js per domain + index.js + game.js + migrations.js
├── data/ # static JS data: tools, materials, recipes, quests, facilities
├── infrastructure/router/ # index.js — routes + beforeEach SEO meta hook
├── shared/
│ ├── layout/ # PageHeader, PageFooter, HomeView, PageMain
│ ├── ui/ # MainCard (reusable)
│ └── utils/rarity.js # getRarityClass(), getRarityLabel()
├── assets/style/ # base.css (CSS vars + .rarity-* classes), main.css
└── __tests__/ # mirrors src/: setup.js + data/ + stores/ + ui/
/ home · /forge · /marche · /codex · /inventaire · /atelier · /profil
/changelog · /guides/debuter-dans-la-forge · /guides/glossaire-forge · /guides/meilleurs-jeux-de-forge
All game routes lazy-load. /:pathMatch(.*)* redirects to /.
Each route has meta: { title, description } — router.beforeEach applies them to document.title and <meta name="description"> automatically.
Section order: <template> → <script> → <style scoped>. 2-space indent, double quotes.
name must exactly match the filename (PascalCase). Self-close void elements: <router-view />.
Options object order: name → components → props → emits → data() → computed → methods → lifecycle hooks.
Access Pinia stores in computed: store() { return useWorkshopStore(); }.
| Type | Convention | Example |
|---|---|---|
| Components / Views | PascalCase .vue |
ShopCard.vue, CraftingView.vue |
| JS modules / stores | camelCase .js |
workshop.js, rarity.js |
| Pinia store ID | camelCase string | defineStore("workshop", …) |
| Methods / variables | camelCase (French OK) | calculerPrix, selectedTool |
| CSS custom properties | --kebab-case |
--auburn, --sea-green, --jet |
- External libraries (
vue,pinia,lucide-vue-next) @/internal aliases (stores, utils, data)- Relative paths (
./,../)
Lazy-load every route: const WorkshopView = () => import("@/domains/workshop/views/WorkshopView.vue");
Options API style — state / getters / actions. One file per domain in src/stores/.
Add persist: true (or { key, paths }) to survive page reload. Plugin wired in src/stores/index.js.
- Tailwind 4.x via
@tailwindcss/vite—@utilityfor custom utilities,@themefor tokens - Use scoped
<style>for component rules; Tailwind classes for layout/spacing/colors - Global CSS vars (
src/assets/style/base.css):--jet(bg) ·--auburn(accent red) ·--dun(warm grey) ·--khaki(muted) ·--viridian(green) ·--sea-green(primary/focus) - Rarity classes (global):
.rarity-common.rarity-uncommon.rarity-rare.rarity-epic.rarity-legendary - Rarity logic: always import
getRarityClass/getRarityLabelfrom@/shared/utils/rarity.js— never inline
Blog views (src/domains/blog/views/) must include <PageHeader /> and <PageFooter /> directly.
Inject application/ld+json in mounted() via document.createElement("script").
Supported types: FAQPage, WebApplication, Article, BreadcrumbList.
Files live in src/__tests__/ mirroring src/, extension .spec.js.
src/__tests__/setup.js globally mocks $router/$route and all lucide-vue-next icons.
Adding a new Lucide icon? Also add it to the mock object in src/__tests__/setup.js or tests throw "No export defined on the mock".
Store tests need: beforeEach(() => { setActivePinia(createPinia()); });
Component tests with <router-link> need: global: { stubs: { RouterLink: { template: "<a><slot /></a>" } } }
- Always Options API — never
<script setup>, never Composition API - No TypeScript — plain
.jsfiles only - French UI content — all user-facing text in French; variable names may be French
- Rarity logic — always import from
@/shared/utils/rarity.js, never duplicate inline - After any change: run
npm run lintthennpm run test:run— both must pass clean - New Lucide icons — add to the mock list in
src/__tests__/setup.js - New routes — add lazy import + route object with
meta.titleandmeta.description - New stores — one file per domain in
src/stores/, Options API style, addpersist: trueif state should survive reload