HoverCard component (new): - Rich preview cards on hover with framer-motion animation - Viewport-aware positioning, portal rendering, open/close delays - UserHoverContent: Discord-style user preview (avatar, bio, stats, follow) - TrackHoverContent: Spotify-style track preview (cover, stats, play) Audio player — Spotify-like 3-column layout: - grid-cols-3 layout: track info | controls | volume+queue - Progress bar moved to top edge (minimal variant) - Glassmorphism (bg-background/95 backdrop-blur-md) - Prominent centered play button (h-10 w-10 rounded-full, active:scale-95) - Title marquee animation for long track names - Reduced padding for tighter premium feel Scrollbar styling: - Migrated hardcoded rgba() to semantic tokens via color-mix(in oklch) - Added transition on thumb hover for smooth visual feedback ContextMenu integration: - TrackListRow wrapped with ContextMenu (play, like, more actions) - Dynamic items based on available callbacks Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.2 KiB
TypeScript
128 lines
3.2 KiB
TypeScript
import { Avatar } from '@/components/ui/avatar';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Users, Music } from 'lucide-react';
|
|
|
|
export interface UserHoverContentProps {
|
|
/** Display name */
|
|
name: string;
|
|
/** Username handle */
|
|
username: string;
|
|
/** Avatar image URL */
|
|
avatar?: string;
|
|
/** Short biography */
|
|
bio?: string;
|
|
/** Number of followers */
|
|
followersCount?: number;
|
|
/** Number of tracks */
|
|
tracksCount?: number;
|
|
/** Whether the current user follows this user */
|
|
isFollowing?: boolean;
|
|
/** Callback when follow button is clicked */
|
|
onFollow?: () => void;
|
|
}
|
|
|
|
/**
|
|
* UserHoverContent - Discord-style user card preview
|
|
*
|
|
* Designed to be used as the `content` prop of `HoverCard`.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <HoverCard
|
|
* content={
|
|
* <UserHoverContent
|
|
* name="Jane Doe"
|
|
* username="janedoe"
|
|
* avatar="/avatars/jane.jpg"
|
|
* bio="Producer & DJ"
|
|
* followersCount={1200}
|
|
* tracksCount={34}
|
|
* />
|
|
* }
|
|
* >
|
|
* <span>@janedoe</span>
|
|
* </HoverCard>
|
|
* ```
|
|
*/
|
|
export function UserHoverContent({
|
|
name,
|
|
username,
|
|
avatar,
|
|
bio,
|
|
followersCount,
|
|
tracksCount,
|
|
isFollowing = false,
|
|
onFollow,
|
|
}: UserHoverContentProps) {
|
|
return (
|
|
<div className="-m-4">
|
|
{/* Cover gradient */}
|
|
<div className="h-12 rounded-t-xl bg-gradient-to-r from-primary/40 to-accent/40" />
|
|
|
|
{/* Avatar + identity */}
|
|
<div className="px-4 -mt-5">
|
|
<Avatar
|
|
src={avatar}
|
|
fallback={name}
|
|
size="lg"
|
|
className="ring-4 ring-popover"
|
|
/>
|
|
|
|
<div className="mt-2">
|
|
<p className="text-sm font-semibold text-foreground leading-tight">
|
|
{name}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">@{username}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bio */}
|
|
{bio && (
|
|
<p className="mt-2 px-4 text-sm text-muted-foreground line-clamp-2">
|
|
{bio}
|
|
</p>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
{(followersCount != null || tracksCount != null) && (
|
|
<div className="mt-3 px-4 flex items-center gap-4 text-xs text-muted-foreground">
|
|
{followersCount != null && (
|
|
<span className="flex items-center gap-1">
|
|
<Users className="size-3.5" />
|
|
<span className="font-medium text-foreground">
|
|
{followersCount.toLocaleString()}
|
|
</span>
|
|
followers
|
|
</span>
|
|
)}
|
|
{tracksCount != null && (
|
|
<span className="flex items-center gap-1">
|
|
<Music className="size-3.5" />
|
|
<span className="font-medium text-foreground">
|
|
{tracksCount.toLocaleString()}
|
|
</span>
|
|
tracks
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Follow button */}
|
|
{onFollow && (
|
|
<div className="mt-3 px-4 pb-4">
|
|
<Button
|
|
variant={isFollowing ? 'outline' : 'default'}
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={onFollow}
|
|
>
|
|
{isFollowing ? 'Following' : 'Follow'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bottom spacing when no follow button */}
|
|
{!onFollow && <div className="pb-4" />}
|
|
</div>
|
|
);
|
|
}
|