# Refonte Graphique — Écran Accouplement > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Refondre l'écran d'accouplement pour passer d'un wizard séquentiel à un layout single-page avec panneaux Parent 1 / Centre / Parent 2 + grille de races, style Obsidienne glassmorphism. **Architecture:** Réécriture complète du HTML généré par `AccouplementView.ts` + remplacement des styles `.accoup-*` dans `components.css` par de nouveaux styles dans `obsidienne.css`. La logique métier (imports Race, CommandBus) reste identique. **Tech Stack:** TypeScript, CSS (tokens Obsidienne/MD3), Material Symbols Outlined, `getDDImage()` helper existant. --- ### Task 1 : Nouveaux styles CSS Obsidienne pour l'accouplement **Files:** - Modify: `src/presentation/styles/obsidienne.css` (ajouter en fin de fichier) - Modify: `src/presentation/styles/components.css:314-401` (supprimer les anciens styles `.accoup-*`) **Contexte:** La maquette utilise du glassmorphism (fond semi-transparent + backdrop-blur + bordure purple subtile), des `rounded-2xl` (16px), des chips pill pour les générations, et un layout grid 12 colonnes. On traduit tout ça en CSS vanilla avec les tokens `--md-*` déjà définis dans `variables.css`. **Step 1 : Supprimer les anciens styles accouplement de components.css** Supprimer les lignes 314-331 et 401 de `components.css` (tout ce qui commence par `.accoup-` et `.accouplement-view`). **Step 2 : Ajouter les nouveaux styles dans obsidienne.css** Ajouter en fin de fichier : ```css /* ── Accouplement View ── */ .accoup-view { display: flex; flex-direction: column; gap: 24px; padding: 8px; } /* Parent Selection Grid (3 columns: parent1 | center | parent2) */ .accoup-parents { display: grid; grid-template-columns: 1fr auto 1fr; gap: 24px; align-items: start; } .accoup-parent-section { display: flex; flex-direction: column; gap: 12px; } .accoup-parent-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 8px; } .accoup-parent-title { font-family: 'Manrope', sans-serif; font-size: 0.95rem; font-weight: 700; color: var(--md-on-surface); text-transform: uppercase; letter-spacing: -0.03em; } .accoup-gender-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; } .accoup-gender-badge.male { color: var(--md-secondary); background: rgba(134, 20, 90, 0.2); } .accoup-gender-badge.female { color: var(--md-primary); background: rgba(193, 133, 253, 0.2); } /* Placeholder card (empty parent slot) */ .accoup-placeholder { height: 180px; border-radius: 16px; background: rgba(23, 23, 33, 0.6); backdrop-filter: blur(12px); border: 1px solid rgba(168, 85, 247, 0.15); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s; } .accoup-placeholder:hover { background: rgba(255, 255, 255, 0.07); } .accoup-placeholder-inner { text-align: center; } .accoup-placeholder-icon { width: 56px; height: 56px; border-radius: 50%; background: rgba(255, 255, 255, 0.05); display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; transition: transform 0.2s; } .accoup-placeholder:hover .accoup-placeholder-icon { transform: scale(1.1); } .accoup-placeholder-icon .material-symbols-outlined { font-size: 28px; opacity: 0.4; } .accoup-placeholder-text { font-size: 11px; color: var(--md-on-surface-variant); } /* Selected parent card (replacing placeholder) */ .accoup-selected-parent { height: 180px; border-radius: 16px; background: rgba(23, 23, 33, 0.6); backdrop-filter: blur(12px); border: 1px solid rgba(168, 85, 247, 0.3); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; position: relative; cursor: pointer; } .accoup-selected-parent:hover { border-color: rgba(168, 85, 247, 0.5); } .accoup-selected-parent .race-card-avatar { width: 64px; height: 64px; } .accoup-selected-parent .race-card-avatar img { width: 64px; height: 64px; } .accoup-selected-parent-name { font-size: 13px; font-weight: 700; color: var(--md-on-surface); } .accoup-selected-parent-badge { font-size: 9px; font-weight: 700; padding: 2px 8px; border-radius: 6px; color: #fff; } .accoup-selected-parent-clear { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border-radius: 50%; background: rgba(255, 255, 255, 0.08); border: none; color: var(--md-on-surface-variant); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: background 0.2s; } .accoup-selected-parent-clear:hover { background: rgba(255, 110, 132, 0.2); color: var(--md-error); } /* Center pairing column */ .accoup-center { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 16px 0; min-width: 160px; } .accoup-heart { width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--md-primary), var(--md-secondary)); display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(203, 151, 255, 0.2); } .accoup-heart .material-symbols-outlined { color: #000; font-size: 22px; } .accoup-center-inputs { display: flex; flex-direction: column; gap: 14px; width: 100%; } .accoup-center-field { display: flex; flex-direction: column; gap: 4px; } .accoup-center-label { display: block; font-size: 10px; font-family: 'Manrope', sans-serif; font-weight: 700; color: var(--md-on-surface-variant); text-transform: uppercase; letter-spacing: 0.08em; text-align: center; } .accoup-center-input { width: 100%; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 8px 12px; text-align: center; font-size: 14px; font-weight: 700; color: var(--md-primary); outline: none; transition: border-color 0.2s, box-shadow 0.2s; } .accoup-center-input:focus { border-color: var(--md-primary); box-shadow: 0 0 0 1px var(--md-primary); } .accoup-center-input.secondary { color: var(--md-secondary); } .accoup-center-input.secondary:focus { border-color: var(--md-secondary); box-shadow: 0 0 0 1px var(--md-secondary); } .accoup-register-btn { padding: 10px 24px; background: linear-gradient(135deg, var(--md-primary), var(--md-primary-container)); color: var(--md-on-primary); font-family: 'Manrope', sans-serif; font-weight: 800; font-size: 11px; border: none; border-radius: 9999px; cursor: pointer; box-shadow: 0 4px 16px rgba(203, 151, 255, 0.3); transition: transform 0.1s, box-shadow 0.2s; text-transform: uppercase; letter-spacing: 0.05em; } .accoup-register-btn:hover { box-shadow: 0 6px 20px rgba(203, 151, 255, 0.4); } .accoup-register-btn:active { transform: scale(0.98); } .accoup-register-btn:disabled { opacity: 0.4; cursor: not-allowed; } /* Grid panel (glass) */ .accoup-grid-panel { background: rgba(23, 23, 33, 0.6); backdrop-filter: blur(12px); border: 1px solid rgba(168, 85, 247, 0.15); border-radius: 24px; padding: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } /* Gen chips */ .accoup-gen-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-bottom: 20px; margin-bottom: 24px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .accoup-gen-chips-label { font-size: 10px; font-weight: 700; color: var(--md-on-surface-variant); text-transform: uppercase; letter-spacing: 0.08em; margin-right: 12px; } .accoup-gen-chip { padding: 6px 16px; border-radius: 9999px; font-size: 11px; font-weight: 700; cursor: pointer; border: none; background: rgba(255, 255, 255, 0.05); color: var(--md-on-surface-variant); transition: background 0.2s, color 0.2s; } .accoup-gen-chip:hover { color: var(--md-on-surface); background: rgba(255, 255, 255, 0.1); } .accoup-gen-chip.active { background: var(--md-primary); color: var(--md-on-primary); } /* Search (kept from existing, restyled) */ .accoup-search-wrap { position: relative; display: flex; align-items: center; margin-bottom: 20px; } .accoup-search { flex: 1; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; color: var(--md-on-surface); padding: 10px 40px 10px 14px; font-size: 13px; font-weight: 600; outline: none; transition: border-color 0.2s; } .accoup-search:focus { border-color: var(--md-primary); } .accoup-search-clear { position: absolute; right: 10px; background: none; border: none; color: var(--md-on-surface-variant); cursor: pointer; font-size: 16px; padding: 4px; transition: color 0.15s; } .accoup-search-clear:hover { color: var(--md-on-surface); } /* Dragon cards grid */ .accoup-race-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; } .accoup-race-card { background: rgba(255, 255, 255, 0.05); border-radius: 16px; padding: 10px; border: 1px solid rgba(255, 255, 255, 0.05); cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s; text-align: center; } .accoup-race-card:hover { border-color: rgba(203, 151, 255, 0.4); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); transform: translateY(-2px); } .accoup-race-card.selecting { border-color: rgba(203, 151, 255, 0.15); } .accoup-race-card.selecting:hover { border-color: rgba(203, 151, 255, 0.5); } .accoup-race-card-img { position: relative; aspect-ratio: 1; border-radius: 12px; background: rgba(255, 255, 255, 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 8px; } .accoup-race-card-img .race-card-avatar { width: 56px; height: 56px; transition: transform 0.2s; } .accoup-race-card:hover .race-card-avatar { transform: scale(1.1); } .accoup-race-card-img .race-card-avatar img { width: 56px; height: 56px; } .accoup-race-card-gen { position: absolute; top: 4px; right: 4px; padding: 2px 6px; border-radius: 6px; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); font-size: 8px; font-weight: 700; color: var(--md-tertiary, #ffe083); } .accoup-race-card-name { font-size: 11px; font-weight: 700; color: var(--md-on-surface); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .accoup-race-card-info { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; } .accoup-race-card-sub { font-size: 9px; color: var(--md-on-surface-variant); } /* Empty state */ .accoup-empty { text-align: center; color: var(--md-on-surface-variant); padding: 40px; font-style: italic; font-size: 13px; } ``` **Step 3 : Vérifier le build** Run: `npm run build` (depuis Windows) Expected: Build réussi sans erreur CSS. **Step 4 : Commit** ```bash git add src/presentation/styles/obsidienne.css src/presentation/styles/components.css git commit -m "refactor(accouplement): remplace anciens styles par design Obsidienne glassmorphism" ``` --- ### Task 2 : Réécriture du HTML — layout single-page avec panneaux parents + grille **Files:** - Modify: `src/presentation/components/AccouplementView.ts` (réécriture complète du rendering) **Contexte:** On remplace le wizard 3 étapes par un layout unique : - **Haut** : grille 3 colonnes (Parent 1 | Centre cœur+inputs+register | Parent 2) - **Bas** : panneau glass avec chips génération + recherche + grille de races - Quand on clique une race, elle remplit le slot parent approprié (P1 si vide, sinon P2) - Quand les 2 parents sont sélectionnés, le bouton Enregistrer devient actif **Step 1 : Réécrire le state et la structure** Remplacer l'interface `AccoupState` et toute la classe : ```typescript import type { CommandBus } from '@application/handlers/CommandBus'; import type { QueryBus } from '@application/handlers/QueryBus'; import { RACES_DATA, GEN_COLORS, RACE_GEN, BREEDING_BY_PARENTS, COMPATIBLE_PARTNERS, raceColor } from '@domain/value-objects/Race'; import { getDDImage } from '@presentation/helpers/dd-image'; import { esc } from '@presentation/helpers/format'; interface AccoupState { parent1: string | null; parent2: string | null; filterGen: number | null; search: string; couples: string; babies: string; selectingSlot: 1 | 2; } export class AccouplementView { private el: HTMLElement | null = null; private accoupState: AccoupState = { parent1: null, parent2: null, filterGen: null, search: '', couples: '', babies: '', selectingSlot: 1, }; private dirty = true; constructor( private commandBus: CommandBus, private queryBus: QueryBus, ) {} render(container: HTMLElement): void { this.el = document.createElement('div'); this.el.className = 'accoup-view'; container.appendChild(this.el); this.dirty = true; this.update(); } update(): void { if (!this.el || !this.dirty) return; this.dirty = false; this.renderSinglePage(); } destroy(): void { this.el?.remove(); this.el = null; } /* ── All races list (cached) ── */ private getAllRaces(): { name: string; gen: number }[] { const all: { name: string; gen: number }[] = []; for (const base of ['Rousse', 'Amande', 'Dorée']) all.push({ name: base, gen: 1 }); for (const [g, rs] of Object.entries(RACES_DATA)) for (const r of rs) all.push({ name: r.name, gen: parseInt(g) }); return all; } /* ── Filtered races based on gen filter, search, and partner compatibility ── */ private getFilteredRaces(): { name: string; gen: number }[] { const { filterGen, search, parent1, selectingSlot } = this.accoupState; let races = this.getAllRaces(); // If selecting parent 2, only show compatible partners if (selectingSlot === 2 && parent1) { const partners = COMPATIBLE_PARTNERS[parent1] ?? []; const partnerNames = new Set(partners.map(p => p.partner)); races = races.filter(r => partnerNames.has(r.name)); } const q = search.trim().toLowerCase(); return races.filter(r => (filterGen ? r.gen === filterGen : true) && (q ? r.name.toLowerCase().includes(q) : true) ); } /* ── Baby name from parents ── */ private getBabyName(): string | null { const { parent1, parent2 } = this.accoupState; if (!parent1 || !parent2) return null; return BREEDING_BY_PARENTS[parent1 + '|' + parent2] ?? null; } /* ── Single page render ── */ private renderSinglePage(): void { if (!this.el) return; const { parent1, parent2, filterGen, search, couples, babies, selectingSlot } = this.accoupState; const baby = this.getBabyName(); const hasBoth = !!(parent1 && parent2 && baby); let html = ''; /* ── Parent panels row ── */ html += `
Cliquer pour choisir un mâle
Cliquer pour choisir une femelle