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>
921 lines
27 KiB
Markdown
Executable File
921 lines
27 KiB
Markdown
Executable File
# 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"
|
||
```
|