- Bulk replace text-white → text-foreground across 116 component files (preserving text-white/ opacity variants) - Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow classes from all components - Replace --duration-normal/--duration-immersive/--duration-slow with --sumi-duration-normal/--sumi-duration-slow across 130+ files - Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out - Replace focus:ring-blue-500 → focus:ring-primary (4 files) - Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms (SUMI anti-pattern: no scale on hover) - Clean up stale kodo- references in comments Co-authored-by: Cursor <cursoragent@cursor.com>
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { useAudio } from '../../context/AudioContext';
|
|
import { Mic2, AlignLeft } from 'lucide-react';
|
|
|
|
export const LyricsPanel: React.FC = () => {
|
|
const { currentTrack, currentTime, seek, duration } = useAudio();
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
|
|
// Auto-scroll logic
|
|
useEffect(() => {
|
|
if (autoScroll && scrollRef.current && currentTrack?.lyrics) {
|
|
const activeIndex = currentTrack.lyrics.findIndex(
|
|
(line: { time: number; text: string }, i: number) => {
|
|
return (
|
|
currentTime >= line.time &&
|
|
(i === currentTrack.lyrics!.length - 1 ||
|
|
currentTime < currentTrack.lyrics![i + 1].time)
|
|
);
|
|
},
|
|
);
|
|
|
|
if (activeIndex !== -1) {
|
|
const element = scrollRef.current.children[activeIndex] as HTMLElement;
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
}
|
|
}, [currentTime, currentTrack, autoScroll]);
|
|
|
|
if (!currentTrack?.lyrics) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground opacity-50">
|
|
<Mic2 className="w-16 h-16 mb-4" />
|
|
<p>No lyrics available</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="h-full flex flex-col relative group"
|
|
onMouseEnter={() => setAutoScroll(false)}
|
|
onMouseLeave={() => setAutoScroll(true)}
|
|
>
|
|
<div className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={() => setAutoScroll(!autoScroll)}
|
|
className={`p-2 rounded-full backdrop-blur-md ${autoScroll ? 'bg-primary/20 text-primary' : 'bg-black/30 text-muted-foreground'}`}
|
|
title="Auto-scroll"
|
|
>
|
|
<AlignLeft className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex-1 overflow-y-auto custom-scrollbar px-4 space-y-6 text-center mask-image-linear-to-b py-24"
|
|
>
|
|
{currentTrack.lyrics.map(
|
|
(line: { time: number; text: string }, i: number) => {
|
|
const isActive =
|
|
currentTime >= line.time &&
|
|
(i === currentTrack.lyrics!.length - 1 ||
|
|
currentTime < currentTrack.lyrics![i + 1].time);
|
|
return (
|
|
<p
|
|
key={i}
|
|
className={`text-2xl md:text-3xl font-bold transition-all duration-[var(--sumi-duration-slow)] cursor-pointer hover:text-foreground ${isActive ? 'text-foreground scale-105 origin-center' : 'text-white/20 blur-[1px]'}`}
|
|
onClick={() => seek((line.time / duration) * 100)}
|
|
>
|
|
{line.text}
|
|
</p>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|