mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
feat: add animated gradient border to Connect Device buttons
Pure CSS conic-gradient animation with @property --border-angle, dynamic accent color from traffic zone, light theme + reduced motion support.
This commit is contained in:
@@ -10,6 +10,7 @@ import { useTheme } from '../../hooks/useTheme';
|
|||||||
import { getTrafficZone } from '../../utils/trafficZone';
|
import { getTrafficZone } from '../../utils/trafficZone';
|
||||||
import { formatTraffic } from '../../utils/formatTraffic';
|
import { formatTraffic } from '../../utils/formatTraffic';
|
||||||
import { getGlassColors } from '../../utils/glassTheme';
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
|
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||||
import type { Subscription } from '../../types';
|
import type { Subscription } from '../../types';
|
||||||
|
|
||||||
interface SubscriptionCardActiveProps {
|
interface SubscriptionCardActiveProps {
|
||||||
@@ -196,15 +197,13 @@ export default function SubscriptionCardActive({
|
|||||||
|
|
||||||
{/* ─── Connect Device Button ─── */}
|
{/* ─── Connect Device Button ─── */}
|
||||||
{subscription.subscription_url && (
|
{subscription.subscription_url && (
|
||||||
<button
|
<HoverBorderGradient
|
||||||
|
as="button"
|
||||||
|
accentColor={zone.mainHex}
|
||||||
onClick={() => navigate('/connection')}
|
onClick={() => navigate('/connection')}
|
||||||
className="mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-all duration-300"
|
className="mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-shadow duration-300"
|
||||||
data-onboarding="connect-devices"
|
data-onboarding="connect-devices"
|
||||||
style={{
|
style={{ fontFamily: 'inherit' }}
|
||||||
fontFamily: 'inherit',
|
|
||||||
background: g.innerBg,
|
|
||||||
border: `1px solid ${g.innerBorder}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Monitor icon */}
|
{/* Monitor icon */}
|
||||||
<div
|
<div
|
||||||
@@ -254,7 +253,7 @@ export default function SubscriptionCardActive({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</HoverBorderGradient>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Stats row: Tariff + Days Left ─── */}
|
{/* ─── Stats row: Tariff + Days Left ─── */}
|
||||||
|
|||||||
@@ -1,30 +1,51 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface HoverBorderGradientProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
/** 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({
|
export function HoverBorderGradient({
|
||||||
children,
|
children,
|
||||||
containerClassName,
|
|
||||||
className,
|
className,
|
||||||
as: Tag = 'button',
|
as: Tag = 'div',
|
||||||
|
accentColor,
|
||||||
|
duration,
|
||||||
|
style,
|
||||||
...props
|
...props
|
||||||
}: React.PropsWithChildren<
|
}: HoverBorderGradientProps) {
|
||||||
{
|
const cssVars: Record<string, string> = {};
|
||||||
as?: React.ElementType;
|
|
||||||
containerClassName?: string;
|
if (accentColor) {
|
||||||
className?: string;
|
cssVars['--accent-color'] = accentColor;
|
||||||
} & React.HTMLAttributes<HTMLElement>
|
// 30% opacity hex suffix for hover glow
|
||||||
>) {
|
cssVars['--accent-glow'] = accentColor + '4D';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration !== undefined) {
|
||||||
|
cssVars['--border-duration'] = `${duration}s`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
className={cn(
|
className={cn('hover-border-gradient', className)}
|
||||||
'hover-border-gradient group flex items-center justify-center rounded-xl text-sm font-medium text-white',
|
style={{ ...cssVars, ...style } as React.CSSProperties}
|
||||||
containerClassName,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className={cn('relative z-10 flex items-center justify-center', className)}>
|
{children}
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getTrafficZone } from '../utils/trafficZone';
|
|||||||
import { formatTraffic } from '../utils/formatTraffic';
|
import { formatTraffic } from '../utils/formatTraffic';
|
||||||
import { getGlassColors } from '../utils/glassTheme';
|
import { getGlassColors } from '../utils/glassTheme';
|
||||||
import { useTheme } from '../hooks/useTheme';
|
import { useTheme } from '../hooks/useTheme';
|
||||||
|
import { HoverBorderGradient } from '../components/ui/hover-border-gradient';
|
||||||
import type {
|
import type {
|
||||||
PurchaseSelection,
|
PurchaseSelection,
|
||||||
PeriodOption,
|
PeriodOption,
|
||||||
@@ -909,14 +910,12 @@ export default function Subscription() {
|
|||||||
|
|
||||||
{/* ─── Connect Device Button ─── */}
|
{/* ─── Connect Device Button ─── */}
|
||||||
{subscription.subscription_url && (
|
{subscription.subscription_url && (
|
||||||
<button
|
<HoverBorderGradient
|
||||||
|
as="button"
|
||||||
|
accentColor={zone.mainHex}
|
||||||
onClick={() => navigate('/connection')}
|
onClick={() => navigate('/connection')}
|
||||||
className="mb-5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-all duration-300"
|
className="mb-5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-shadow duration-300"
|
||||||
style={{
|
style={{ fontFamily: 'inherit' }}
|
||||||
background: g.innerBg,
|
|
||||||
border: `1px solid ${g.innerBorder}`,
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] transition-colors duration-500"
|
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] transition-colors duration-500"
|
||||||
@@ -961,7 +960,7 @@ export default function Subscription() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</HoverBorderGradient>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Subscription URL ─── */}
|
{/* ─── Subscription URL ─── */}
|
||||||
|
|||||||
@@ -10,10 +10,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hover-border-gradient {
|
.hover-border-gradient {
|
||||||
--_bg: rgb(var(--color-dark-900));
|
--_bg: var(--border-inner-bg, rgb(var(--color-dark-900)));
|
||||||
--_accent: rgb(var(--color-accent-400));
|
--_accent: var(--accent-color, rgb(var(--color-accent-400)));
|
||||||
border: 1.5px solid transparent;
|
border: 1.5px solid transparent;
|
||||||
padding: 10px 16px;
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--_bg), var(--_bg)) padding-box,
|
linear-gradient(var(--_bg), var(--_bg)) padding-box,
|
||||||
conic-gradient(
|
conic-gradient(
|
||||||
@@ -24,12 +23,22 @@
|
|||||||
var(--_accent) 100%
|
var(--_accent) 100%
|
||||||
)
|
)
|
||||||
border-box;
|
border-box;
|
||||||
animation: border-rotate 3s linear infinite;
|
animation: border-rotate var(--border-duration, 3s) linear infinite;
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-border-gradient:hover {
|
.hover-border-gradient:hover,
|
||||||
box-shadow: 0 0 16px rgba(var(--color-accent-400), 0.3);
|
.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 {
|
@keyframes border-rotate {
|
||||||
|
|||||||
Reference in New Issue
Block a user