refactor(ui): Design tokens - gradients, duration, textarea

- Replace cyan/magenta/purple gradients with primary/secondary
- duration-200/300 → duration-[var(--duration-normal)]
- Textarea: min-h-[100px] → min-h-24
- SearchPageHeader, DashboardPage, PlaylistHeader
- UserProfilePageHeader/Hero, PlaylistDetailPageHero
- SocialViewFeedItem, WishlistView, PostCard, ProductCard, CourseCard
- SearchPageResults, MarketplaceHome

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-10 22:56:30 +01:00
parent 2c6e5fddb1
commit c65da4fea6
15 changed files with 22 additions and 22 deletions

View file

@ -155,7 +155,7 @@ export const WishlistView: React.FC = () => {
<motion.div key={product.id} variants={cardVariants}>
<Card
variant="default"
className="p-4 group hover:border-border/50 hover:-translate-y-1 hover:shadow-lg transition-all duration-200"
className="p-4 group hover:border-border/50 hover:-translate-y-1 hover:shadow-lg transition-all duration-[var(--duration-normal)]"
>
<div className="flex gap-4">
<div className="relative w-24 h-24 bg-muted rounded-lg overflow-hidden flex-shrink-0">

View file

@ -27,14 +27,14 @@ const CourseCardComponent: React.FC<CourseCardProps> = ({
return (
<Card
variant="default"
className="group p-0 overflow-hidden cursor-pointer hover:border-border/80 hover:-translate-y-1 hover:shadow-xl transition-all duration-200 flex flex-col h-full"
className="group p-0 overflow-hidden cursor-pointer hover:border-border/80 hover:-translate-y-1 hover:shadow-xl transition-all duration-[var(--duration-normal)] flex flex-col h-full"
onClick={() => onClick(course)}
>
{/* Cover */}
<div className="relative aspect-video bg-kodo-ink overflow-hidden">
<OptimizedImage
src={course.thumbnailUrl}
className="w-full h-full object-cover opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300"
className="w-full h-full object-cover opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-[var(--duration-normal)]"
alt={course.title}
/>
<div className="absolute inset-0 bg-background/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">

View file

@ -41,7 +41,7 @@ const ProductCardComponent: React.FC<ProductCardProps> = ({
<div className="relative aspect-square overflow-hidden bg-black">
<OptimizedImage
src={product.coverUrl || (product as any).cover_url || 'https://picsum.photos/400'}
className={`w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ${isPlayingPreview ? 'scale-110 blur-sm opacity-50' : ''}`}
className={`w-full h-full object-cover group-hover:scale-105 transition-transform duration-[var(--duration-normal)] ${isPlayingPreview ? 'scale-110 blur-sm opacity-50' : ''}`}
alt={product.title}
/>

View file

@ -112,7 +112,7 @@ const PostCardComponent: React.FC<PostCardProps> = ({ post }) => {
<>
<Card
variant="default"
className="p-0 overflow-hidden border-transparent hover:border-primary/20 hover:-translate-y-0.5 hover:shadow-lg transition-all duration-200 animate-fadeIn mb-4"
className="p-0 overflow-hidden border-transparent hover:border-primary/20 hover:-translate-y-0.5 hover:shadow-lg transition-all duration-[var(--duration-normal)] animate-fadeIn mb-4"
>
{/* Repost Header */}
{post.isRepost && (
@ -177,7 +177,7 @@ const PostCardComponent: React.FC<PostCardProps> = ({ post }) => {
<OptimizedImage
src={post.image!}
alt={post.content?.substring(0, 50) || 'Post image'}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
className="w-full h-full object-cover transition-transform duration-[var(--duration-normal)] group-hover:scale-105"
/>
</div>
)}
@ -189,7 +189,7 @@ const PostCardComponent: React.FC<PostCardProps> = ({ post }) => {
<img
src={post.audioTrack.coverUrl}
alt={post.audioTrack.title || 'Track cover'}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
className="w-full h-full object-cover transition-transform duration-[var(--duration-normal)] group-hover:scale-110"
/>
<div className="absolute inset-0 bg-background/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Play className="w-5 h-5 text-foreground fill-current" />

View file

@ -89,7 +89,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
'rounded-lg',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'transition-all duration-[var(--duration-fast)]',
'min-h-[100px] resize-y',
'min-h-24 resize-y',
className,
)}
{...props}

View file

@ -39,7 +39,7 @@ export function SocialViewFeedItem({ track, onPlay }: SocialViewFeedItemProps) {
>
<Card
variant="glass"
className="p-0 overflow-hidden border-white/5 bg-black/20 backdrop-blur-xl hover:border-primary/20 hover-glow-cyan transition-all duration-200"
className="p-0 overflow-hidden border-white/5 bg-black/20 backdrop-blur-xl hover:border-primary/20 hover-glow-cyan transition-all duration-[var(--duration-normal)]"
>
<div className="p-4 flex items-center gap-3">
<Avatar
@ -72,7 +72,7 @@ export function SocialViewFeedItem({ track, onPlay }: SocialViewFeedItemProps) {
<img
src={track.coverUrl}
alt=""
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
className="w-full h-full object-cover transition-transform duration-[var(--duration-normal)] group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Play className="w-6 h-6 text-white fill-current" />

View file

@ -44,7 +44,7 @@ function WelcomeBanner({ username }: { username: string }) {
<div className="relative z-10">
<h1 className="text-heading-1">
{greeting},{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-500 to-magenta-500">
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">
{username}
</span>
</h1>
@ -73,7 +73,7 @@ function QuickActions() {
<Link
key={action.label}
to={action.path}
className="group flex items-center gap-3 p-4 rounded-xl border border-border hover:border-primary/30 hover:bg-muted/50 transition-all duration-200 animate-stagger-in"
className="group flex items-center gap-3 p-4 rounded-xl border border-border hover:border-primary/30 hover:bg-muted/50 transition-all duration-[var(--duration-normal)] animate-stagger-in"
style={{ animationDelay: `${i * 60}ms` }}
>
<div className={cn('p-2.5 rounded-lg', action.color)}>

View file

@ -33,7 +33,7 @@ export function PlaylistHeader({ playlist, className }: PlaylistHeaderProps) {
<CardContent className="p-4 sm:p-6">
<div className="flex flex-col md:flex-row gap-4 sm:gap-6">
{/* Cover Image - Mobile optimized */}
<div className="relative w-full md:w-64 h-48 sm:h-64 flex-shrink-0 rounded-lg overflow-hidden bg-gradient-to-br from-purple-500 to-pink-500">
<div className="relative w-full md:w-64 h-48 sm:h-64 flex-shrink-0 rounded-lg overflow-hidden bg-gradient-to-br from-primary/40 to-secondary/40">
{playlist.cover_url ? (
<img
src={playlist.cover_url}

View file

@ -25,7 +25,7 @@ export function PlaylistDetailPageCoverAndInfo({ playlist }: PlaylistDetailPageC
className="w-full h-full object-cover rounded-xl shadow-inner group-hover:scale-105 transition-transform duration-[var(--duration-slower)]"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-cyan-900 to-magenta-900 rounded-xl flex items-center justify-center">
<div className="w-full h-full bg-gradient-to-br from-primary/30 to-secondary/30 rounded-xl flex items-center justify-center">
<Music className="w-20 h-20 text-white/20" />
</div>
)}

View file

@ -7,7 +7,7 @@ interface PlaylistDetailPageHeroProps {
export function PlaylistDetailPageHero({ playlist }: PlaylistDetailPageHeroProps) {
return (
<div className="relative h-80 md:h-96 w-full overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/50 via-black to-magenta-900/50" />
<div className="absolute inset-0 bg-gradient-to-br from-primary/30 via-background to-secondary/30" />
{playlist.cover_url && (
<div
className="absolute inset-0 opacity-30 blur-3xl scale-110"

View file

@ -40,11 +40,11 @@ export function UserProfilePageHeader({
{/* Avatar with animated glow ring */}
<div className="relative -mt-16 md:-mt-20 group">
<div
className="absolute -inset-1 rounded-3xl bg-gradient-to-br from-cyan-400 via-primary to-magenta-500 blur-xl opacity-50 group-hover:opacity-100 transition-opacity duration-[var(--duration-slow)]"
className="absolute -inset-1 rounded-3xl bg-gradient-to-br from-primary via-primary/80 to-secondary blur-xl opacity-50 group-hover:opacity-100 transition-opacity duration-[var(--duration-slow)]"
aria-hidden
/>
<div
className="absolute -inset-0.5 rounded-3xl bg-gradient-to-br from-cyan-400 to-magenta-500 opacity-70"
className="absolute -inset-0.5 rounded-3xl bg-gradient-to-br from-primary to-secondary opacity-70"
aria-hidden
/>
<Avatar

View file

@ -4,7 +4,7 @@ export function UserProfilePageHero() {
return (
<div className="h-64 md:h-80 w-full relative overflow-hidden">
{/* Base gradient layer */}
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/50 via-purple-900/40 to-magenta-900/50" />
<div className="absolute inset-0 bg-gradient-to-br from-primary/30 via-primary/20 to-secondary/30" />
{/* Radial highlight at center-top */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/20 via-transparent to-transparent" />

View file

@ -35,7 +35,7 @@ export function SearchPageHeader({
<button
type="button"
onClick={onClear}
className="p-2 mr-2 hover:bg-muted/50 rounded-full transition-colors text-muted-foreground hover:text-foreground"
className="p-2 mr-2 hover:bg-muted/50 rounded-full transition-colors duration-[var(--duration-fast)] text-muted-foreground hover:text-foreground"
aria-label="Clear search"
>
<X className="w-5 h-5" />

View file

@ -193,7 +193,7 @@ export function SearchPageResults({ results, query = '' }: SearchPageResultsProp
onClick={() => navigate(`/playlists/${playlist.id}`)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/playlists/${playlist.id}`); } }}
>
<div className="h-32 bg-gradient-to-br from-purple-900 to-black relative">
<div className="h-32 bg-gradient-to-br from-primary/30 to-background relative">
{playlist.cover_url && (
<img
src={playlist.cover_url}

View file

@ -34,7 +34,7 @@ function MarketplaceHomeSkeleton() {
return (
<div className="min-h-screen pb-24 relative">
{/* Background Ambience */}
<div className="absolute inset-x-0 top-0 h-96 bg-gradient-to-b from-purple-900/20 via-background to-background pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-96 bg-gradient-to-b from-primary/20 via-background to-background pointer-events-none" />
<div className="container mx-auto px-4 py-8 relative z-10">
{/* Header */}
@ -192,7 +192,7 @@ export function MarketplaceHome() {
return (
<div className="min-h-screen pb-24 relative">
{/* Background Ambience */}
<div className="absolute inset-x-0 top-0 h-96 bg-gradient-to-b from-purple-900/20 via-background to-background pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-96 bg-gradient-to-b from-primary/20 via-background to-background pointer-events-none" />
<div className="container mx-auto px-4 py-8 relative z-10">
{mutationError && (