dd-timer/docs/plans/2026-04-04-accouplement-redesign.md
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

921 lines
27 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 += `<div class="accoup-parents">`;
// Parent 1 section
html += `<section class="accoup-parent-section">`;
html += `<div class="accoup-parent-header">
<span class="accoup-parent-title">Parent 1</span>
<span class="accoup-gender-badge male">MÂLE REQUIS</span>
</div>`;
if (parent1) {
const gen1 = RACE_GEN[parent1] ?? 1;
html += `<div class="accoup-selected-parent" data-clear="1">
${getDDImage(parent1)}
<span class="accoup-selected-parent-name">${esc(parent1)}</span>
<span class="accoup-selected-parent-badge" style="background:${GEN_COLORS[gen1] ?? '#888'}">Gen ${gen1}</span>
<button class="accoup-selected-parent-clear" data-clear="1" title="Retirer">
<span class="material-symbols-outlined" style="font-size:14px">close</span>
</button>
</div>`;
} else {
html += `<div class="accoup-placeholder" data-select-slot="1">
<div class="accoup-placeholder-inner">
<div class="accoup-placeholder-icon">
<span class="material-symbols-outlined" style="color:var(--md-primary)">add</span>
</div>
<p class="accoup-placeholder-text">Cliquer pour choisir un mâle</p>
</div>
</div>`;
}
html += `</section>`;
// Center column
html += `<div class="accoup-center">`;
html += `<div class="accoup-heart">
<span class="material-symbols-outlined mso-fill">favorite</span>
</div>`;
html += `<div class="accoup-center-inputs">
<div class="accoup-center-field">
<label class="accoup-center-label">Nombre de couples</label>
<input class="accoup-center-input" id="accoup-couples" type="number" min="1" value="${esc(couples)}" placeholder="1">
</div>
<div class="accoup-center-field">
<label class="accoup-center-label">Bébés obtenus</label>
<input class="accoup-center-input secondary" id="accoup-babies" type="number" min="0" value="${esc(babies)}" placeholder="0">
</div>
</div>`;
// Baby preview (if both parents selected)
if (hasBoth && baby) {
const babyGen = RACE_GEN[baby] ?? 0;
html += `<div style="text-align:center;margin-top:4px">
<div style="font-size:10px;color:var(--md-on-surface-variant);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:6px">Résultat</div>
${getDDImage(baby)}
<div style="font-size:12px;font-weight:700;color:var(--md-on-surface);margin-top:4px">${esc(baby)}</div>
<span class="accoup-selected-parent-badge" style="background:${GEN_COLORS[babyGen] ?? '#888'};font-size:8px">Gen ${babyGen}</span>
</div>`;
}
html += `<button class="accoup-register-btn" id="accoup-register" ${hasBoth ? '' : 'disabled'}>ENREGISTRER</button>`;
html += `</div>`;
// Parent 2 section
html += `<section class="accoup-parent-section">`;
html += `<div class="accoup-parent-header">
<span class="accoup-parent-title">Parent 2</span>
<span class="accoup-gender-badge female">FEMELLE REQUISE</span>
</div>`;
if (parent2) {
const gen2 = RACE_GEN[parent2] ?? 1;
html += `<div class="accoup-selected-parent" data-clear="2">
${getDDImage(parent2)}
<span class="accoup-selected-parent-name">${esc(parent2)}</span>
<span class="accoup-selected-parent-badge" style="background:${GEN_COLORS[gen2] ?? '#888'}">Gen ${gen2}</span>
<button class="accoup-selected-parent-clear" data-clear="2" title="Retirer">
<span class="material-symbols-outlined" style="font-size:14px">close</span>
</button>
</div>`;
} else {
html += `<div class="accoup-placeholder" data-select-slot="2">
<div class="accoup-placeholder-inner">
<div class="accoup-placeholder-icon">
<span class="material-symbols-outlined" style="color:var(--md-secondary)">add</span>
</div>
<p class="accoup-placeholder-text">Cliquer pour choisir une femelle</p>
</div>
</div>`;
}
html += `</section>`;
html += `</div>`; // .accoup-parents
/* ── Grid panel ── */
html += `<div class="accoup-grid-panel">`;
// Gen chips
html += `<div class="accoup-gen-chips">`;
html += `<span class="accoup-gen-chips-label">Générations</span>`;
html += `<button class="accoup-gen-chip${filterGen === null ? ' active' : ''}" data-gen="all">Toutes</button>`;
for (let g = 1; g <= 10; g++) {
html += `<button class="accoup-gen-chip${filterGen === g ? ' active' : ''}" data-gen="${g}">Gen ${g}</button>`;
}
html += `</div>`;
// Search
html += `<div class="accoup-search-wrap">
<input class="accoup-search" id="accoup-search-input" type="text"
placeholder="Rechercher une race…" value="${esc(search)}" autocomplete="off">
${search ? `<button class="accoup-search-clear" title="Effacer"><span class="material-symbols-outlined" style="font-size:16px">close</span></button>` : ''}
</div>`;
// Race grid
const filtered = this.getFilteredRaces();
if (filtered.length === 0) {
html += `<div class="accoup-empty">Aucune race trouvée</div>`;
} else {
const selecting = (!parent1 || !parent2);
html += `<div class="accoup-race-grid">`;
for (const race of filtered) {
const genCol = GEN_COLORS[race.gen] ?? '#888';
html += `<div class="accoup-race-card${selecting ? ' selecting' : ''}" data-race="${esc(race.name)}">
<div class="accoup-race-card-img">
${getDDImage(race.name)}
<div class="accoup-race-card-gen" style="color:${genCol}">GEN ${race.gen}</div>
</div>
<div class="accoup-race-card-name">${esc(race.name)}</div>
</div>`;
}
html += `</div>`;
}
html += `</div>`; // .accoup-grid-panel
this.el.innerHTML = html;
this.bindEvents();
// Restore search focus if active
if (search) {
const inp = this.el.querySelector<HTMLInputElement>('#accoup-search-input');
if (inp) { inp.focus(); inp.setSelectionRange(inp.value.length, inp.value.length); }
}
}
/* ── Event binding ── */
private bindEvents(): void {
if (!this.el) return;
// Gen chips
this.el.querySelectorAll('.accoup-gen-chip').forEach(btn => {
btn.addEventListener('click', () => {
const val = (btn as HTMLElement).dataset.gen;
this.accoupState.filterGen = val === 'all' ? null : parseInt(val!);
this.dirty = true; this.update();
});
});
// Search
const searchInput = this.el.querySelector<HTMLInputElement>('#accoup-search-input');
if (searchInput) {
searchInput.addEventListener('input', () => {
this.accoupState.search = searchInput.value;
this.dirty = true; this.update();
});
}
const clearBtn = this.el.querySelector('.accoup-search-clear');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
this.accoupState.search = '';
this.dirty = true; this.update();
});
}
// Placeholder clicks (select slot)
this.el.querySelectorAll<HTMLElement>('[data-select-slot]').forEach(ph => {
ph.addEventListener('click', () => {
this.accoupState.selectingSlot = parseInt(ph.dataset.selectSlot!) as 1 | 2;
// No re-render needed, just changes which slot gets filled
});
});
// Clear parent buttons
this.el.querySelectorAll<HTMLElement>('[data-clear]').forEach(btn => {
if (btn.tagName === 'BUTTON') {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const slot = parseInt(btn.dataset.clear!);
if (slot === 1) {
this.accoupState.parent1 = null;
this.accoupState.parent2 = null; // Reset P2 too since partners depend on P1
this.accoupState.selectingSlot = 1;
} else {
this.accoupState.parent2 = null;
this.accoupState.selectingSlot = 2;
}
this.dirty = true; this.update();
});
}
});
// Race card clicks
this.el.querySelectorAll<HTMLElement>('.accoup-race-card').forEach(card => {
card.addEventListener('click', () => {
const race = card.dataset.race!;
if (!this.accoupState.parent1) {
this.accoupState.parent1 = race;
this.accoupState.selectingSlot = 2;
this.accoupState.filterGen = null;
this.accoupState.search = '';
} else if (!this.accoupState.parent2) {
this.accoupState.parent2 = race;
}
this.dirty = true; this.update();
});
});
// Couples / babies inputs
const couplesInput = this.el.querySelector<HTMLInputElement>('#accoup-couples');
const babiesInput = this.el.querySelector<HTMLInputElement>('#accoup-babies');
if (couplesInput) {
let prev = couplesInput.value;
couplesInput.addEventListener('focus', () => { prev = couplesInput.value; couplesInput.value = ''; });
couplesInput.addEventListener('blur', () => {
if (couplesInput.value === '') couplesInput.value = prev;
this.accoupState.couples = couplesInput.value;
});
}
if (babiesInput) {
let prev = babiesInput.value;
babiesInput.addEventListener('focus', () => { prev = babiesInput.value; babiesInput.value = ''; });
babiesInput.addEventListener('blur', () => {
if (babiesInput.value === '') babiesInput.value = prev;
this.accoupState.babies = babiesInput.value;
});
}
// Register button
const registerBtn = this.el.querySelector('#accoup-register');
if (registerBtn) {
registerBtn.addEventListener('click', () => {
const { parent1, parent2 } = this.accoupState;
if (!parent1 || !parent2) return;
const baby = BREEDING_BY_PARENTS[parent1 + '|' + parent2] ?? '';
if (!baby) return;
const c = parseInt(this.accoupState.couples) || 0;
const b = parseInt(this.accoupState.babies) || 0;
if (c <= 0) return;
this.commandBus.execute({
type: 'register-accouplement',
parent1,
parent2,
baby,
gen: RACE_GEN[baby] ?? 0,
couples: c,
babiesObtained: b,
});
// Reset
this.accoupState = {
parent1: null, parent2: null,
filterGen: null, search: '',
couples: '', babies: '',
selectingSlot: 1,
};
this.dirty = true; this.update();
});
}
}
}
```
**Step 2 : Vérifier la compilation**
Run: `npm run build` (depuis Windows)
Expected: Build réussi, pas d'erreurs TypeScript.
**Step 3 : Tester visuellement**
Run: `npm start`
- Naviguer vers l'écran Accouplement
- Vérifier le layout 3 colonnes (Parent 1 | Cœur+inputs | Parent 2)
- Cliquer une race → remplit Parent 1
- La grille se filtre pour montrer les partenaires compatibles
- Cliquer un partenaire → remplit Parent 2
- Le bébé apparaît au centre + bouton Enregistrer actif
- Le bouton × vide un parent
- Les chips génération et la recherche fonctionnent
**Step 4 : Commit**
```bash
git add src/presentation/components/AccouplementView.ts
git commit -m "feat(accouplement): refonte layout single-page Obsidienne avec panneaux parents"
```
---
### Task 3 : Nettoyage et polish
**Files:**
- Modify: `src/presentation/styles/components.css` (vérifier qu'il ne reste rien d'accoup)
- Verify: `src/presentation/components/AccouplementView.ts`
**Step 1 : Vérifier qu'aucun ancien style `.accoup-*` ne reste dans components.css**
Chercher `accoup` dans `components.css`. Si des restes existent, les supprimer.
**Step 2 : Vérifier que le flow complet fonctionne**
Test manuel :
1. Sélectionner "Rousse" → P1 se remplit, grille montre partenaires compatibles
2. Sélectionner un partenaire → P2 se remplit, bébé apparaît au centre
3. Entrer couples=2, bébés=1
4. Cliquer "Enregistrer" → reset complet
5. Bouton × sur P1 → reset P1 et P2
6. Bouton × sur P2 → reset P2 seulement
7. Filtrer par Gen 2 → seules les races Gen 2 apparaissent
8. Rechercher "Rou" → filtre textuel fonctionne
**Step 3 : Commit final**
```bash
git add -A
git commit -m "chore(accouplement): nettoyage styles et polish refonte"
```