veza/apps/web/src/features/player/components/RepeatShuffleButtons.tsx
senke aeade50247 feat(ui): tooltip adoption + search highlighting & skeleton loading
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>
2026-02-09 23:14:00 +01:00

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;