feat: animation temporelle des flux Ğ1
ci/woodpecker/push/woodpecker Pipeline was successful

Nouveau mode animation accessible via "▶ Animer" dans le sélecteur de période.
- useAnimation : hook gérant frames, lecture, vitesse, filtrage client
- AnimationPlayer : barre de contrôle (play/pause, slider, ×1/×2/×4)
- Granularité auto : 24 frames/h (24h), 7 frames/jour (7j), ~4 frames/semaine (30j)
- Stats et heatmap mis à jour sur la fenêtre courante, zéro requête réseau supplémentaire

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-23 20:29:25 +01:00
parent 26e429c8c0
commit 7975abc619
5 changed files with 292 additions and 8 deletions
+109
View File
@@ -0,0 +1,109 @@
import type { TimeFrame } from '../hooks/useAnimation';
interface AnimationPlayerProps {
frames: TimeFrame[];
currentIndex: number;
playing: boolean;
speed: 1 | 2 | 4;
onSeek: (i: number) => void;
onPlay: () => void;
onPause: () => void;
onSpeedChange: (s: 1 | 2 | 4) => void;
onClose: () => void;
}
export function AnimationPlayer({
frames,
currentIndex,
playing,
speed,
onSeek,
onPlay,
onPause,
onSpeedChange,
onClose,
}: AnimationPlayerProps) {
const frame = frames[currentIndex];
return (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-[1001] w-[min(640px,90vw)]">
<div className="bg-[#0a0b0f]/90 backdrop-blur-sm border border-[#2e2f3a] rounded-2xl px-5 py-3 flex flex-col gap-2.5 shadow-xl">
{/* Frame label + position */}
<div className="flex items-center justify-between">
<span className="text-[#d4a843] text-sm font-medium">
{frame?.label ?? '—'}
</span>
<span className="text-[#4b5563] text-xs tabular-nums">
{currentIndex + 1} / {frames.length}
</span>
</div>
{/* Slider */}
<input
type="range"
min={0}
max={frames.length - 1}
value={currentIndex}
onChange={(e) => onSeek(Number(e.target.value))}
className="w-full h-1 accent-[#d4a843] cursor-pointer"
/>
{/* Controls row */}
<div className="flex items-center justify-between">
{/* Playback buttons */}
<div className="flex items-center gap-1">
<button
onClick={() => onSeek(Math.max(0, currentIndex - 1))}
className="px-2.5 py-1.5 text-[#6b7280] hover:text-white transition-colors text-sm"
title="Frame précédente"
>
</button>
<button
onClick={playing ? onPause : onPlay}
className="px-4 py-1.5 bg-[#d4a843] text-[#0a0b0f] rounded-lg font-bold text-sm hover:bg-[#e8c060] transition-colors min-w-[52px] text-center shadow-[0_0_10px_rgba(212,168,67,0.3)]"
>
{playing ? '⏸' : '▶'}
</button>
<button
onClick={() => onSeek(Math.min(frames.length - 1, currentIndex + 1))}
className="px-2.5 py-1.5 text-[#6b7280] hover:text-white transition-colors text-sm"
title="Frame suivante"
>
</button>
</div>
{/* Speed selector */}
<div className="flex items-center gap-1">
<span className="text-[#4b5563] text-xs mr-1">Vitesse</span>
{([1, 2, 4] as const).map((s) => (
<button
key={s}
onClick={() => onSpeedChange(s)}
className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
speed === s
? 'bg-[#d4a843] text-[#0a0b0f]'
: 'text-[#6b7280] hover:text-[#d4a843] hover:bg-[#1a1b23]'
}`}
>
×{s}
</button>
))}
</div>
{/* Close */}
<button
onClick={onClose}
className="text-[#4b5563] hover:text-white transition-colors px-2 py-1 text-sm ml-2"
title="Quitter l'animation"
>
</button>
</div>
</div>
</div>
);
}