- README : fonctionnalités, installation, build, tests (302 + 20 E2E), couverture 94%, workflow mise à jour latest.yml, changelog v1.1.6 - CLAUDE.md : règles de collaboration, architecture, conventions - Plans de conception : DDD, electron-updater, accouplement, toast Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 KiB
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 :
/* ── 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
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 :
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
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 :
- Sélectionner "Rousse" → P1 se remplit, grille montre partenaires compatibles
- Sélectionner un partenaire → P2 se remplit, bébé apparaît au centre
- Entrer couples=2, bébés=1
- Cliquer "Enregistrer" → reset complet
- Bouton × sur P1 → reset P1 et P2
- Bouton × sur P2 → reset P2 seulement
- Filtrer par Gen 2 → seules les races Gen 2 apparaissent
- Rechercher "Rou" → filtre textuel fonctionne
Step 3 : Commit final
git add -A
git commit -m "chore(accouplement): nettoyage styles et polish refonte"