diff --git a/src/main.jsx b/src/main.jsx
index 5d363d7..ea609d2 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Chess } from 'chess.js';
-import { Brain, ChevronRight, CircleHelp, FlipVertical2, Lightbulb, LoaderCircle, RotateCcw, Settings, Sparkles, X } from 'lucide-react';
+import { Brain, ChevronRight, CircleHelp, FlipVertical2, Lightbulb, LoaderCircle, Pause, Play, RotateCcw, Settings, SkipBack, SkipForward, Sparkles, X } from 'lucide-react';
import './styles.css';
const PIECES = { p: '♟', n: '♞', b: '♝', r: '♜', q: '♛', k: '♚', P: '♟', N: '♞', B: '♝', R: '♜', Q: '♛', K: '♚' };
@@ -103,6 +103,9 @@ function App() {
const [explaining, setExplaining] = useState(false);
const [showHint, setShowHint] = useState(true);
const [showSettings, setShowSettings] = useState(false);
+ const [previewLine, setPreviewLine] = useState(null);
+ const [previewStep, setPreviewStep] = useState(0);
+ const [previewPlaying, setPreviewPlaying] = useState(false);
const [settings, setSettings] = useState(() => {
try { return { ...defaultSettings, ...JSON.parse(localStorage.getItem('atelier64-settings')) }; } catch { return defaultSettings; }
});
@@ -110,6 +113,17 @@ function App() {
const targets = selected ? game.moves({ square: selected, verbose: true }).map((m) => m.to) : [];
const bestMove = showHint ? analysis[0]?.pv?.[0] : null;
const lastMove = moves.at(-1)?.uci;
+ const previewVariation = previewLine === null ? null : analysis[previewLine];
+ const previewGame = useMemo(() => {
+ const replay = new Chess(fen);
+ for (const move of previewVariation?.moves?.slice(0, previewStep) || []) {
+ try { replay.move({ from: move.uci.slice(0, 2), to: move.uci.slice(2, 4), promotion: move.uci[4] || 'q' }); } catch { break; }
+ }
+ return replay;
+ }, [fen, previewVariation, previewStep]);
+ const displayedGame = previewVariation ? previewGame : game;
+ const previewLastMove = previewVariation?.moves?.[previewStep - 1]?.uci;
+ const previewNextMove = previewVariation?.moves?.[previewStep]?.uci;
useEffect(() => {
const controller = new AbortController();
@@ -126,6 +140,28 @@ function App() {
return () => { clearTimeout(timer); controller.abort(); };
}, [fen, settings.depth]);
+ useEffect(() => {
+ if (!previewPlaying || !previewVariation) return;
+ if (previewStep >= previewVariation.moves.length) {
+ setPreviewPlaying(false);
+ return;
+ }
+ const timer = setTimeout(() => setPreviewStep((step) => step + 1), 900);
+ return () => clearTimeout(timer);
+ }, [previewPlaying, previewStep, previewVariation]);
+
+ useEffect(() => {
+ setPreviewLine(null); setPreviewStep(0); setPreviewPlaying(false);
+ }, [fen]);
+
+ function openVariation(index) {
+ setPreviewLine(index); setPreviewStep(1); setPreviewPlaying(true); setSelected(null);
+ }
+
+ function closeVariation() {
+ setPreviewLine(null); setPreviewStep(0); setPreviewPlaying(false);
+ }
+
function attemptMove(from, to) {
const next = new Chess(fen);
try {
@@ -183,7 +219,8 @@ function App() {
PARTIE LIBRE · DEUX CÔTÉS
{status}
{game.turn() === 'w' ? 'BLANCS' : 'NOIRS'}
-
+ {previewVariation && VARIANTE {String(previewLine + 1).padStart(2, '0')}{previewStep}/{previewVariation.moves.length}
}
+ {} : onSquare}/>
@@ -201,7 +238,14 @@ function App() {
VARIANTES PRINCIPALESÉVAL.
- {analysis.slice(0, 3).map((line, index) => {String(index + 1).padStart(2, '0')}{line.moves?.slice(0, 6).map((m, i) => {m.san} )}
{scoreLabel(line.score)} )}
+ {analysis.slice(0, 3).map((line, index) => )}
+ {previewVariation &&
+
+
+
+
{previewStep ? previewVariation.moves[previewStep - 1]?.san : 'Position initiale'}
+
+
}
diff --git a/src/styles.css b/src/styles.css
index d3eb441..e05d807 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -20,7 +20,8 @@ main { width:min(1450px,94vw); margin:0 auto; padding:34px 0 45px; display:grid;
.eyebrow { font:10px 'DM Mono',monospace;letter-spacing:.19em;color:var(--copper); }
h1,h2 { font-family:'Playfair Display',serif;margin:4px 0 0;font-weight:600; } h1 { font-size:clamp(24px,2.1vw,34px); } h2 { font-size:27px; }
.turn-chip { font:10px 'DM Mono',monospace;letter-spacing:.12em;display:flex;align-items:center;gap:8px;padding-bottom:5px; }.mini-white,.mini-black { width:12px;height:12px;border-radius:50%;display:block;box-shadow:0 0 0 1px var(--ink); }.mini-white{background:var(--cream)}.mini-black{background:var(--ink)}
-.board-area { display:flex; gap:16px; align-items:stretch; }
+.board-area { display:flex; gap:16px; align-items:stretch;position:relative; }
+.preview-ribbon{position:absolute;z-index:8;top:10px;left:44px;background:rgba(17,20,17,.92);color:var(--cream);padding:8px 9px 8px 12px;display:flex;align-items:center;gap:11px;box-shadow:0 6px 18px rgba(0,0,0,.2);font:9px 'DM Mono',monospace;letter-spacing:.13em}.preview-ribbon b{color:#e99567}.preview-ribbon button{border:0;border-left:1px solid #4b4e45;background:transparent;color:var(--cream);padding:0 0 0 8px;display:grid;place-items:center;cursor:pointer}
.eval-bar { width:18px; position:relative; display:flex;flex-direction:column;gap:8px; }.eval-number { font:9px 'DM Mono',monospace; writing-mode:vertical-rl;transform:rotate(180deg);height:34px; }.eval-track { background:var(--ink);flex:1;position:relative;overflow:hidden; }.eval-white { position:absolute;bottom:0;width:100%;background:var(--cream);transition:height .55s cubic-bezier(.2,.8,.2,1); }
.board-shell { width:min(100%,calc(100vh - 220px)); aspect-ratio:1; box-shadow:0 25px 50px rgba(55,43,27,.18),0 2px 0 rgba(255,255,255,.4) inset; border:7px solid #292a24; }
.board { display:grid;grid-template-columns:repeat(8,minmax(0,1fr));grid-template-rows:repeat(8,minmax(0,1fr));position:relative;width:100%;height:100%;border:1px solid #10110f; }
@@ -34,7 +35,8 @@ h1,h2 { font-family:'Playfair Display',serif;margin:4px 0 0;font-weight:600; } h
.coach-column { border-left:1px solid var(--line);padding-left:clamp(28px,4vw,60px);min-width:0; }
.coach-title { display:flex;align-items:center;gap:14px;margin:4px 0 23px; }.coach-orb{width:48px;height:48px;border-radius:50%;display:grid;place-items:center;background:var(--ink);color:var(--paper);box-shadow:0 0 0 7px rgba(17,20,17,.06)}.analysis-spinner{margin-left:auto;color:var(--copper)}
.best-card { background:var(--ink);color:var(--cream);padding:22px 25px 24px;position:relative;overflow:hidden; }.best-card:after{content:'♞';font:130px 'Times New Roman';position:absolute;right:-12px;bottom:-48px;color:rgba(255,255,255,.035);}.card-index{font:10px 'DM Mono';color:var(--copper);margin-right:13px}.card-label{font:10px 'DM Mono';letter-spacing:.18em;color:#a9aa9f}.best-move{display:flex;align-items:baseline;gap:13px;margin-top:19px}.best-move strong{font:italic 46px 'Playfair Display',serif}.best-move span{font:12px 'DM Mono';padding:4px 7px;background:var(--copper);}.best-card p{font-size:12px;color:#aaa99f;margin:2px 0 16px}.hint-toggle{position:relative;z-index:2;border:1px solid #45473f;background:transparent;color:#bbbcb1;padding:9px 12px;display:flex;align-items:center;gap:7px;cursor:pointer;font-size:11px}.hint-toggle.active{color:#f0bb68;border-color:#8b7148}.empty-analysis{padding:34px 0 23px;color:#96978d;font-family:'Playfair Display',serif;font-style:italic}
-.variations { margin-top:26px; }.section-label{display:flex;justify-content:space-between;border-bottom:1px solid var(--ink);padding-bottom:8px;font:9px 'DM Mono';letter-spacing:.15em;color:#69665e}.variation{display:grid;grid-template-columns:30px 1fr 42px;gap:10px;align-items:start;border-bottom:1px solid var(--line);padding:14px 2px;font-size:13px;line-height:1.7}.variation-number{font:10px 'DM Mono';color:var(--copper);padding-top:4px}.variation b:last-child{text-align:right;font:500 11px 'DM Mono';padding-top:4px}.first-san{font-weight:800;color:var(--copper)}
+.variations { margin-top:26px; }.section-label{display:flex;justify-content:space-between;border-bottom:1px solid var(--ink);padding-bottom:8px;font:9px 'DM Mono';letter-spacing:.15em;color:#69665e}.variation{width:100%;font-family:inherit;text-align:left;background:transparent;cursor:pointer;display:grid;grid-template-columns:30px 1fr 42px;gap:10px;align-items:start;border:0;border-bottom:1px solid var(--line);padding:14px 2px;font-size:13px;line-height:1.7;transition:background .18s,padding .18s}.variation:hover{background:rgba(199,109,69,.08);padding-left:8px}.variation.active{background:var(--ink);color:var(--cream);padding-left:10px;padding-right:8px}.variation-number{font:10px 'DM Mono';color:var(--copper);padding-top:4px}.variation-moves{display:block}.variation b:last-child{text-align:right;font:500 11px 'DM Mono';padding-top:4px}.first-san{font-weight:800;color:var(--copper)}
+.variation-player{display:flex;align-items:center;gap:5px;padding:11px 8px;background:#ded5c3;border-bottom:1px solid var(--ink)}.variation-player button{width:30px;height:30px;border:1px solid rgba(17,20,17,.25);background:transparent;display:grid;place-items:center;cursor:pointer}.variation-player .player-main{background:var(--copper);border-color:var(--copper);color:white}.variation-player>span{font:11px 'DM Mono';margin-left:7px;color:#5d594f}.variation-player .close-player{width:auto;margin-left:auto;padding:0 5px;border:0;border-bottom:1px solid var(--ink);font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.08em}
.coach-note { background:#ded5c3;margin-top:26px;padding:20px 22px;border-left:3px solid var(--copper); }.note-heading{display:flex;align-items:center;gap:8px;font:10px 'DM Mono';letter-spacing:.15em}.coach-note p{font-family:'Playfair Display',serif;font-size:15px;line-height:1.55;margin:13px 0;color:#3b3a34}.explain-button{border:0;border-bottom:1px solid #383933;background:transparent;padding:5px 0;display:flex;align-items:center;gap:7px;text-transform:uppercase;font-size:10px;font-weight:800;letter-spacing:.09em;cursor:pointer}.explain-button svg:last-child{margin-left:4px}.explain-button:disabled{opacity:.5}
.moves-panel{margin-top:27px}.move-list{max-height:155px;overflow:auto;padding-top:6px}.move-row{display:grid;grid-template-columns:35px 1fr 1fr;padding:6px 2px;font-size:12px}.move-row span{font:10px 'DM Mono';color:#8d887d}.move-row b{font-weight:600}.moves-empty{font:italic 14px 'Playfair Display';color:#8a857a;display:block;padding:16px 0}.error-banner{font-size:11px;padding:11px 13px;margin-top:15px;background:#b94c3d;color:white}
footer { border-top:1px solid var(--line);padding:18px 4vw;display:flex;justify-content:space-between;color:#8d877b;font:9px 'DM Mono';letter-spacing:.12em; }