Tooltip adoption (18 conversions across 11 files): - Player controls: shuffle, repeat, mute, expand, close, lyrics, auto-scroll - Navbar: theme toggle - File browser: download, add tag, AI auto-tag, watermark, process with AI - Notifications: mark as read - Share links: open link, revoke link - Chat: scroll to bottom Search polish: - New highlightMatch utility — wraps matching text in <mark> with primary color - Applied to track titles, artist names, playlist names in SearchPageResults - Applied to suggestion dropdown titles and subtitles - Replaced spinner loading state with content-aware SearchPageSkeleton - Skeleton matches actual results layout (tab bar, track cards, artist circles) Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
/**
|
|
* Composant RepeatShuffleButtons
|
|
* Boutons pour contrôler le mode repeat et shuffle
|
|
*/
|
|
|
|
import { Repeat, Shuffle } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Tooltip } from '@/components/ui/tooltip';
|
|
|
|
export interface RepeatShuffleButtonsProps {
|
|
repeat: 'off' | 'track' | 'playlist';
|
|
shuffle: boolean;
|
|
onRepeatChange: (repeat: 'off' | 'track' | 'playlist') => void;
|
|
onShuffleToggle: () => void;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
variant?: 'default' | 'ghost' | 'outline';
|
|
}
|
|
|
|
export function RepeatShuffleButtons({
|
|
repeat,
|
|
shuffle,
|
|
onRepeatChange,
|
|
onShuffleToggle,
|
|
className,
|
|
disabled = false,
|
|
size = 'md',
|
|
variant = 'ghost',
|
|
}: RepeatShuffleButtonsProps) {
|
|
const sizeClasses = {
|
|
sm: 'h-8 w-8',
|
|
md: 'h-10 w-10',
|
|
lg: 'h-12 w-12',
|
|
};
|
|
|
|
const iconSizes = {
|
|
sm: 'h-4 w-4',
|
|
md: 'h-5 w-5',
|
|
lg: 'h-6 w-6',
|
|
};
|
|
|
|
const variantClasses = {
|
|
default:
|
|
'bg-primary text-primary-foreground hover:bg-primary focus:ring-primary',
|
|
ghost:
|
|
'bg-transparent text-foreground hover:bg-muted focus:ring-muted',
|
|
outline:
|
|
'border border-border bg-card text-foreground hover:bg-muted focus:ring-muted',
|
|
};
|
|
|
|
const handleRepeatClick = () => {
|
|
if (disabled) return;
|
|
|
|
// Cycle: off -> track -> playlist -> off
|
|
if (repeat === 'off') {
|
|
onRepeatChange('track');
|
|
} else if (repeat === 'track') {
|
|
onRepeatChange('playlist');
|
|
} else {
|
|
onRepeatChange('off');
|
|
}
|
|
};
|
|
|
|
const getRepeatLabel = () => {
|
|
switch (repeat) {
|
|
case 'track':
|
|
return 'Répéter la piste';
|
|
case 'playlist':
|
|
return 'Répéter la playlist';
|
|
default:
|
|
return 'Répéter désactivé';
|
|
}
|
|
};
|
|
|
|
const getRepeatAriaLabel = () => {
|
|
switch (repeat) {
|
|
case 'track':
|
|
return 'Répéter la piste (actif)';
|
|
case 'playlist':
|
|
return 'Répéter la playlist (actif)';
|
|
default:
|
|
return 'Répéter désactivé';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn('flex items-center gap-2', className)}>
|
|
{/* Repeat Button */}
|
|
<Tooltip content={getRepeatLabel()} disabled={disabled}>
|
|
<button
|
|
type="button"
|
|
onClick={handleRepeatClick}
|
|
disabled={disabled}
|
|
className={cn(
|
|
'rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 relative',
|
|
sizeClasses[size],
|
|
variantClasses[variant],
|
|
repeat !== 'off' &&
|
|
'bg-primary text-primary-foreground hover:bg-primary',
|
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
)}
|
|
aria-label={getRepeatAriaLabel()}
|
|
aria-pressed={repeat !== 'off'}
|
|
aria-disabled={disabled}
|
|
>
|
|
<Repeat
|
|
className={cn(iconSizes[size], repeat === 'track' && 'fill-current')}
|
|
aria-hidden="true"
|
|
/>
|
|
{repeat === 'playlist' && (
|
|
<span
|
|
className="absolute bottom-0 right-0 text-[8px] font-bold leading-none bg-primary rounded-full w-3 h-3 flex items-center justify-center"
|
|
aria-hidden="true"
|
|
>
|
|
1
|
|
</span>
|
|
)}
|
|
<span className="sr-only">{getRepeatLabel()}</span>
|
|
</button>
|
|
</Tooltip>
|
|
|
|
{/* Shuffle Button */}
|
|
<Tooltip content={shuffle ? 'Mélanger activé' : 'Mélanger désactivé'} disabled={disabled}>
|
|
<button
|
|
type="button"
|
|
onClick={onShuffleToggle}
|
|
disabled={disabled}
|
|
className={cn(
|
|
'rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
|
|
sizeClasses[size],
|
|
variantClasses[variant],
|
|
shuffle &&
|
|
'bg-primary text-primary-foreground hover:bg-primary',
|
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
)}
|
|
aria-label={shuffle ? 'Mélanger activé' : 'Mélanger désactivé'}
|
|
aria-pressed={shuffle}
|
|
aria-disabled={disabled}
|
|
>
|
|
<Shuffle
|
|
className={cn(iconSizes[size], shuffle && 'fill-current')}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="sr-only">
|
|
{shuffle ? 'Mélanger activé' : 'Mélanger désactivé'}
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default RepeatShuffleButtons;
|