feat: add Radix HoverCard dependency and wrap tabs with HoverCard for

This commit is contained in:
Nexmoe
2025-11-15 10:55:00 +08:00
parent 5af393ef84
commit 448a15fce4
6 changed files with 355 additions and 274 deletions

View File

@@ -30,6 +30,7 @@
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",

33
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.16
version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-hover-card':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-label':
specifier: ^2.1.7
version: 2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -802,6 +805,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-hover-card@1.1.15':
resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
@@ -4162,6 +4178,23 @@ snapshots:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)

View File

@@ -0,0 +1,35 @@
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn } from '@renderer/lib/utils'
import type * as React from 'react'
function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
}
function HoverCardContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -112,21 +112,15 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
return (
<div key={item.id} className="flex flex-col items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => onPageChange(item.id)}
className={`no-drag w-12 h-12 ${isActive ? 'bg-primary/10' : ''}`}
>
<IconComponent className={`h-5! w-5! ${isActive ? 'text-primary' : ''}`} />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>{item.label}</p>
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="icon"
onClick={() => onPageChange(item.id)}
className={`no-drag w-12 h-12 ${isActive ? 'bg-primary/10' : ''}`}
>
<IconComponent className={`h-5! w-5! ${isActive ? 'text-primary' : ''}`} />
</Button>
{showLabel && (
<span className="text-xs text-muted-foreground text-center leading-tight px-3">
{item.label}

View File

@@ -142,6 +142,7 @@ export function Home({ onOpenSupportedSites, onOpenSettings }: HomeProps) {
const [url, setUrl] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const [activeTab, setActiveTab] = useState<'single' | 'playlist'>('single')
const inlinePreviewSites = popularSites
.slice(0, 3)
.map((site) => t(`sites.popular.${site.id}.label`))
@@ -536,262 +537,275 @@ export function Home({ onOpenSupportedSites, onOpenSettings }: HomeProps) {
className="container mx-auto max-w-7xl p-6 space-y-6 overflow-hidden w-full"
style={{ maxWidth: '100%' }}
>
<Tabs defaultValue="single" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="single" className="flex items-center gap-2">
{t('download.singleVideo')}
</TabsTrigger>
<TabsTrigger value="playlist" className="flex items-center gap-2">
{t('playlist.title')}
</TabsTrigger>
</TabsList>
{/* Single Video Download Tab */}
<TabsContent value="single" className="space-y-6">
{/* URL Input Card */}
{!videoInfo && (
<Card>
<CardHeader>
<CardTitle>{t('download.enterUrl')}</CardTitle>
<Card>
<Tabs
defaultValue="single"
value={activeTab}
onValueChange={(value) => setActiveTab(value as 'single' | 'playlist')}
className="w-full gap-0"
>
<CardHeader>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<CardTitle>
{activeTab === 'single' ? t('download.enterUrl') : t('playlist.enterPlaylistUrl')}
</CardTitle>
<CardDescription>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>{t('sites.homeInlineDescription', { sites: inlinePreviewSites })}</span>
<Button
type="button"
variant="link"
className="px-0"
onClick={() => onOpenSupportedSites?.()}
>
{t('sites.viewAll')}
</Button>
</div>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<div className="relative flex-1">
<Input
ref={inputRef}
placeholder={t('download.urlPlaceholder')}
value={url}
onChange={(e) => setUrl(e.target.value)}
onKeyDown={handleKeyDown}
className="pr-10"
disabled={loading}
/>
{loading && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
<Button onClick={handlePasteUrl} variant="outline" disabled={loading}>
{t('download.paste')}
</Button>
{settings.oneClickDownload ? (
<Button onClick={handleOneClickDownload} disabled={loading || !url.trim()}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('download.loading')}
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
{t('download.oneClickDownloadNow')}
</>
)}
</Button>
) : (
<Button onClick={handleFetchVideo} disabled={loading || !url.trim()}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('download.loading')}
</>
) : (
<>
<Search className="mr-2 h-4 w-4" />
{t('download.fetch')}
</>
)}
</Button>
)}
</div>
{/* One-Click Download Info */}
{settings.oneClickDownload && (
<div className="flex items-center justify-between gap-2 rounded-lg bg-card px-4 py-3 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span>{t('download.oneClickDownloadEnabled')}</span>
</div>
{onOpenSettings && (
{activeTab === 'single' ? (
<div className="flex flex-wrap items-center gap-2">
<span>{t('sites.homeInlineDescription', { sites: inlinePreviewSites })}</span>
<Button
type="button"
variant="link"
className="h-auto px-2 py-0 text-xs"
onClick={onOpenSettings}
className="p-0 h-auto"
onClick={() => onOpenSupportedSites?.()}
>
{t('download.goToSettings')}
{t('sites.viewAll')}
</Button>
</div>
) : (
t('playlist.playlistUrlDescription')
)}
</CardDescription>
</div>
<TabsList className="grid grid-cols-2">
<TabsTrigger value="single" className="flex items-center gap-2">
{t('download.singleVideo')}
</TabsTrigger>
<TabsTrigger value="playlist" className="flex items-center gap-2">
{t('playlist.title')}
</TabsTrigger>
</TabsList>
</div>
</CardHeader>
<CardContent>
{/* Single Video Download Tab */}
<TabsContent value="single" className="space-y-6 mt-0">
{/* URL Input Card */}
{!videoInfo && (
<div className="space-y-4">
<div className="flex gap-2">
<div className="relative flex-1">
<Input
ref={inputRef}
placeholder={t('download.urlPlaceholder')}
value={url}
onChange={(e) => setUrl(e.target.value)}
onKeyDown={handleKeyDown}
className="pr-10"
disabled={loading}
/>
{loading && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
<Button onClick={handlePasteUrl} variant="outline" disabled={loading}>
{t('download.paste')}
</Button>
{settings.oneClickDownload ? (
<Button onClick={handleOneClickDownload} disabled={loading || !url.trim()}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('download.loading')}
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
{t('download.oneClickDownloadNow')}
</>
)}
</Button>
) : (
<Button onClick={handleFetchVideo} disabled={loading || !url.trim()}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('download.loading')}
</>
) : (
<>
<Search className="mr-2 h-4 w-4" />
{t('download.fetch')}
</>
)}
</Button>
)}
</div>
)}
{/* Error Display */}
{error && (
<div className="rounded-lg border border-destructive bg-destructive/10 p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-destructive mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm font-medium text-destructive">
{t('errors.fetchInfoFailed')}
</p>
<p className="text-sm text-muted-foreground">{error}</p>
{/* One-Click Download Info */}
{settings.oneClickDownload && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span>{t('download.oneClickDownloadEnabled')}</span>
</div>
{onOpenSettings && (
<Button
type="button"
variant="link"
className="h-auto px-2 py-0 text-xs"
onClick={onOpenSettings}
>
{t('download.goToSettings')}
</Button>
)}
</div>
)}
{/* Error Display */}
{error && (
<div className="rounded-lg border border-destructive bg-destructive/10 p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-destructive mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm font-medium text-destructive">
{t('errors.fetchInfoFailed')}
</p>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
</div>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Video Info and Download Options */}
{videoInfo && !loading && <VideoInfoCard videoInfo={videoInfo} />}
</TabsContent>
{/* Playlist Download Tab */}
<TabsContent value="playlist" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('playlist.enterPlaylistUrl')}</CardTitle>
<CardDescription>{t('playlist.playlistUrlDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor={playlistUrlId}>{t('playlist.linkLabel')}</Label>
<div className="flex gap-2">
<Input
id={playlistUrlId}
placeholder="https://www.youtube.com/playlist?list=..."
value={playlistUrl}
onChange={(e) => {
setPlaylistUrl(e.target.value)
setPlaylistInfo(null)
setPlaylistPreviewError(null)
}}
className="flex-1"
disabled={playlistBusy}
/>
<Button
onClick={handlePastePlaylistUrl}
variant="outline"
disabled={playlistBusy}
>
{t('download.paste')}
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label>
<Select
value={downloadType}
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
disabled={playlistBusy}
>
<SelectTrigger id={downloadTypeId}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="video">{t('download.video')}</SelectItem>
<SelectItem value="audio">{t('download.audio')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground">{t('playlist.range')}</Label>
<div className="grid grid-cols-2 gap-2">
<Input
type="number"
placeholder={t('playlist.startIndex')}
value={startIndex}
onChange={(e) => setStartIndex(e.target.value)}
min="1"
disabled={playlistBusy}
/>
<Input
type="number"
placeholder={t('playlist.endIndex')}
value={endIndex}
onChange={(e) => setEndIndex(e.target.value)}
min="1"
disabled={playlistBusy}
/>
</div>
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Button
onClick={handlePreviewPlaylist}
variant="outline"
className="w-full sm:w-auto"
disabled={playlistBusy || !playlistUrl.trim()}
>
{playlistPreviewLoading ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
{t('download.loading')}
</>
) : (
<>
<Search className="mr-2 h-5 w-5" />
{t('playlist.previewButton')}
</>
)}
</Button>
{playlistInfo && (
</div>
)}
{/* Video Info and Download Options */}
{videoInfo && !loading && <VideoInfoCard videoInfo={videoInfo} />}
</TabsContent>
{/* Playlist Download Tab */}
<TabsContent value="playlist" className="space-y-6 mt-0">
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor={playlistUrlId}>{t('playlist.linkLabel')}</Label>
<div className="flex gap-2">
<Input
id={playlistUrlId}
placeholder="https://www.youtube.com/playlist?list=..."
value={playlistUrl}
onChange={(e) => {
setPlaylistUrl(e.target.value)
setPlaylistInfo(null)
setPlaylistPreviewError(null)
}}
className="flex-1"
disabled={playlistBusy}
/>
<Button
onClick={handlePastePlaylistUrl}
variant="outline"
disabled={playlistBusy}
>
{t('download.paste')}
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label>
<Select
value={downloadType}
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
disabled={playlistBusy}
>
<SelectTrigger id={downloadTypeId}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="video">{t('download.video')}</SelectItem>
<SelectItem value="audio">{t('download.audio')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground">{t('playlist.range')}</Label>
<div className="grid grid-cols-2 gap-2">
<Input
type="number"
placeholder={t('playlist.startIndex')}
value={startIndex}
onChange={(e) => setStartIndex(e.target.value)}
min="1"
disabled={playlistBusy}
/>
<Input
type="number"
placeholder={t('playlist.endIndex')}
value={endIndex}
onChange={(e) => setEndIndex(e.target.value)}
min="1"
disabled={playlistBusy}
/>
</div>
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Button
onClick={handleDownloadPlaylist}
className="w-full sm:flex-1"
size="lg"
disabled={playlistDownloadLoading || !playlistUrl.trim()}
onClick={handlePreviewPlaylist}
variant="outline"
className="w-full sm:w-auto"
disabled={playlistBusy || !playlistUrl.trim()}
>
{playlistDownloadLoading ? (
{playlistPreviewLoading ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
{t('download.loading')}
</>
) : (
t('playlist.downloadPlaylist')
<>
<Search className="mr-2 h-5 w-5" />
{t('playlist.previewButton')}
</>
)}
</Button>
{playlistInfo && (
<Button
onClick={handleDownloadPlaylist}
className="w-full sm:flex-1"
size="lg"
disabled={playlistDownloadLoading || !playlistUrl.trim()}
>
{playlistDownloadLoading ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
{t('download.loading')}
</>
) : (
t('playlist.downloadPlaylist')
)}
</Button>
)}
</div>
{playlistUrl.trim() && !playlistInfo && !playlistPreviewError && !playlistBusy && (
<p className="text-xs text-muted-foreground">{t('playlist.previewRequired')}</p>
)}
{playlistPreviewError && (
<div className="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{playlistPreviewError}
</div>
)}
</div>
</TabsContent>
</CardContent>
</Tabs>
</Card>
{playlistUrl.trim() && !playlistInfo && !playlistPreviewError && !playlistBusy && (
<p className="text-xs text-muted-foreground">{t('playlist.previewRequired')}</p>
)}
{playlistPreviewError && (
<div className="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{playlistPreviewError}
</div>
)}
</CardContent>
</Card>
{playlistInfo && (
<PlaylistPreviewCard
playlist={playlistInfo}
entries={selectedPlaylistEntries}
onClear={handleClearPlaylistPreview}
/>
)}
</TabsContent>
</Tabs>
{/* Playlist Preview Card (outside main card) */}
{playlistInfo && (
<PlaylistPreviewCard
playlist={playlistInfo}
entries={selectedPlaylistEntries}
onClear={handleClearPlaylistPreview}
/>
)}
{/* Unified Download History */}
<UnifiedDownloadHistory />

View File

@@ -18,6 +18,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger
} from '@renderer/components/ui/context-menu'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card'
import { RemoteImage } from '@renderer/components/ui/remote-image'
import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
@@ -127,16 +128,16 @@ function SubscriptionTab({
return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
<TabsTrigger
value={subscription.id}
className={cn(
'flex h-auto w-20 flex-col rounded-sm! items-center gap-1 px-2 py-2 transition-all hover:opacity-80 shrink-0 grow-0',
isActive && 'bg-muted/45'
)}
>
<Tooltip>
<TooltipTrigger asChild>
<HoverCard openDelay={0} closeDelay={0}>
<ContextMenuTrigger asChild>
<HoverCardTrigger asChild>
<TabsTrigger
value={subscription.id}
className={cn(
'flex h-auto w-20 flex-col rounded-sm! items-center gap-1 px-2 py-2 transition-all hover:opacity-80 shrink-0 grow-0',
isActive && 'bg-muted/45'
)}
>
<div className="relative h-12 w-12 shrink-0 overflow-hidden transition-colors">
<RemoteImage
src={subscription.coverUrl}
@@ -150,21 +151,24 @@ function SubscriptionTab({
)}
/>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs space-y-1">
<p className="text-xs">{statusDescription}</p>
<p className="text-xs">
{t('subscriptions.status.tooltip.updatedAt', { time: lastUpdatedLabel })}
</p>
</TooltipContent>
</Tooltip>
<div className="flex w-full flex-col items-center text-center">
<span className="w-full truncate text-xs font-medium">
{subscription.title || t('subscriptions.labels.unknown')}
</span>
</div>
</TabsTrigger>
</ContextMenuTrigger>
<div className="flex w-full flex-col items-center text-center">
<span className="w-full truncate text-xs font-medium">
{subscription.title || t('subscriptions.labels.unknown')}
</span>
</div>
</TabsTrigger>
</HoverCardTrigger>
</ContextMenuTrigger>
<HoverCardContent className="max-w-xs space-y-1">
<p className="text-sm font-semibold">
{subscription.title || t('subscriptions.labels.unknown')}
</p>
<p className="text-xs">{statusDescription}</p>
<p className="text-xs">
{t('subscriptions.status.tooltip.updatedAt', { time: lastUpdatedLabel })}
</p>
</HoverCardContent>
</HoverCard>
<ContextMenuContent>
<ContextMenuItem onClick={handleRefresh}>
<RefreshCw className="h-4 w-4" />
@@ -557,7 +561,7 @@ function SubscriptionCard({ subscription }: { subscription: SubscriptionRule })
{t('subscriptions.labels.noThumbnail')}
</div>
)}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-black/5 to-transparent" />
<div className="pointer-events-none absolute inset-0 bg-linear-to-t from-black/70 via-black/5 to-transparent" />
<div className="absolute top-3 left-3 flex items-center gap-2 rounded-full bg-black/60 pr-3 pl-1 py-1 text-xs font-medium text-white backdrop-blur">
{subscription.coverUrl ? (
<div className="h-6 w-6 overflow-hidden rounded-full border border-white/40">
@@ -572,7 +576,7 @@ function SubscriptionCard({ subscription }: { subscription: SubscriptionRule })
{(subscription.title || t('subscriptions.labels.unknown')).slice(0, 1)}
</div>
)}
<span className="max-w-[10rem] truncate text-xs">
<span className="max-w-40 truncate text-xs">
{subscription.title || t('subscriptions.labels.unknown')}
</span>
</div>