veza/apps/web/src/components/ui/hover-card/UserHoverContent.tsx
senke b1f5fbf0bf feat(ui): hover cards, Spotify player layout, scrollbar tokens, context menu integration
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>
2026-02-09 23:18:46 +01:00

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>
);
}