diff --git a/src/components/dashboard/SubscriptionCardActive.tsx b/src/components/dashboard/SubscriptionCardActive.tsx index 0ca4e8c..099faaf 100644 --- a/src/components/dashboard/SubscriptionCardActive.tsx +++ b/src/components/dashboard/SubscriptionCardActive.tsx @@ -10,6 +10,7 @@ import { useTheme } from '../../hooks/useTheme'; import { getTrafficZone } from '../../utils/trafficZone'; import { formatTraffic } from '../../utils/formatTraffic'; import { getGlassColors } from '../../utils/glassTheme'; +import { HoverBorderGradient } from '../ui/hover-border-gradient'; import type { Subscription } from '../../types'; interface SubscriptionCardActiveProps { @@ -196,15 +197,13 @@ export default function SubscriptionCardActive({ {/* ─── Connect Device Button ─── */} {subscription.subscription_url && ( - + )} {/* ─── Stats row: Tariff + Days Left ─── */} diff --git a/src/components/ui/hover-border-gradient.tsx b/src/components/ui/hover-border-gradient.tsx index 16deb68..a4302c6 100644 --- a/src/components/ui/hover-border-gradient.tsx +++ b/src/components/ui/hover-border-gradient.tsx @@ -1,30 +1,51 @@ import React from 'react'; import { cn } from '@/lib/utils'; +interface HoverBorderGradientProps extends React.HTMLAttributes { + /** Rendered HTML element. Default: 'div' */ + as?: React.ElementType; + /** Hex color for the rotating gradient beam (e.g. '#00E5A0') */ + accentColor?: string; + /** Full rotation duration in seconds. Default: 3 */ + duration?: number; +} + +/** + * Animated conic-gradient border that rotates around the element. + * + * Uses pure CSS `@property --border-angle` animation — no JS loops, + * GPU-friendly, works in Telegram Mini App WebView. + * + * Fallback: browsers without `@property` show a static gradient border. + */ export function HoverBorderGradient({ children, - containerClassName, className, - as: Tag = 'button', + as: Tag = 'div', + accentColor, + duration, + style, ...props -}: React.PropsWithChildren< - { - as?: React.ElementType; - containerClassName?: string; - className?: string; - } & React.HTMLAttributes ->) { +}: HoverBorderGradientProps) { + const cssVars: Record = {}; + + if (accentColor) { + cssVars['--accent-color'] = accentColor; + // 30% opacity hex suffix for hover glow + cssVars['--accent-glow'] = accentColor + '4D'; + } + + if (duration !== undefined) { + cssVars['--border-duration'] = `${duration}s`; + } + return ( - - {children} - + {children} ); } diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 7582779..a416c3f 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -10,6 +10,7 @@ import { getTrafficZone } from '../utils/trafficZone'; import { formatTraffic } from '../utils/formatTraffic'; import { getGlassColors } from '../utils/glassTheme'; import { useTheme } from '../hooks/useTheme'; +import { HoverBorderGradient } from '../components/ui/hover-border-gradient'; import type { PurchaseSelection, PeriodOption, @@ -909,14 +910,12 @@ export default function Subscription() { {/* ─── Connect Device Button ─── */} {subscription.subscription_url && ( - + )} {/* ─── Subscription URL ─── */} diff --git a/src/styles/globals.css b/src/styles/globals.css index deeb7f3..f6f16bc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -10,10 +10,9 @@ } .hover-border-gradient { - --_bg: rgb(var(--color-dark-900)); - --_accent: rgb(var(--color-accent-400)); + --_bg: var(--border-inner-bg, rgb(var(--color-dark-900))); + --_accent: var(--accent-color, rgb(var(--color-accent-400))); border: 1.5px solid transparent; - padding: 10px 16px; background: linear-gradient(var(--_bg), var(--_bg)) padding-box, conic-gradient( @@ -24,12 +23,22 @@ var(--_accent) 100% ) border-box; - animation: border-rotate 3s linear infinite; - transition: box-shadow 0.3s ease; + animation: border-rotate var(--border-duration, 3s) linear infinite; } -.hover-border-gradient:hover { - box-shadow: 0 0 16px rgba(var(--color-accent-400), 0.3); +.hover-border-gradient:hover, +.hover-border-gradient:active { + box-shadow: 0 0 20px var(--accent-glow, rgba(var(--color-accent-400), 0.25)); +} + +.light .hover-border-gradient { + --_bg: var(--border-inner-bg, rgb(var(--color-champagne-100))); +} + +@media (prefers-reduced-motion: reduce) { + .hover-border-gradient { + animation-play-state: paused; + } } @keyframes border-rotate {