mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix: eliminate hover flickering across all pages
- Extract CountdownTimer into React.memo component to isolate 1s interval re-renders from the full Subscription page - Add box-shadow transition to .hover-border-gradient (was instant) - Remove conflicting p-[1.5px] from PurchaseCTAButton (CSS border already handles 1.5px) - Replace transition-all with specific properties on bento-card-hover, bento-card-glow, device dots, toggle switches, progress bars - Reduce translateY hover from -4px to -2px to prevent cursor losing hover target during fast vertical mouse movement
This commit is contained in:
@@ -32,10 +32,10 @@ export default function PurchaseCTAButton({ subscription }: PurchaseCTAButtonPro
|
|||||||
<HoverBorderGradient
|
<HoverBorderGradient
|
||||||
accentColor={accentColor}
|
accentColor={accentColor}
|
||||||
duration={4}
|
duration={4}
|
||||||
className="group relative w-full cursor-pointer overflow-hidden rounded-2xl p-[1.5px]"
|
className="group relative w-full cursor-pointer overflow-hidden rounded-2xl"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative flex items-center justify-between rounded-[14px] px-5 py-4 transition-all duration-300"
|
className="relative flex items-center justify-between rounded-[14px] px-5 py-4 transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: isExpired
|
background: isExpired
|
||||||
? 'linear-gradient(135deg, rgba(255,59,92,0.08), rgba(255,107,53,0.06))'
|
? 'linear-gradient(135deg, rgba(255,59,92,0.08), rgba(255,107,53,0.06))'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback, memo } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
@@ -20,6 +20,146 @@ import {
|
|||||||
getFlagEmoji,
|
getFlagEmoji,
|
||||||
} from '../utils/subscriptionHelpers';
|
} from '../utils/subscriptionHelpers';
|
||||||
|
|
||||||
|
/** Isolated countdown so 1s interval doesn't re-render the whole page */
|
||||||
|
const CountdownTimer = memo(function CountdownTimer({
|
||||||
|
endDate,
|
||||||
|
isActive,
|
||||||
|
glassColors: g,
|
||||||
|
}: {
|
||||||
|
endDate: string;
|
||||||
|
isActive: boolean;
|
||||||
|
glassColors: ReturnType<typeof getGlassColors>;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const endTime = new Date(endDate).getTime();
|
||||||
|
const tick = () => {
|
||||||
|
const diff = Math.max(0, endTime - Date.now());
|
||||||
|
setCountdown({
|
||||||
|
days: Math.floor(diff / 86_400_000),
|
||||||
|
hours: Math.floor((diff % 86_400_000) / 3_600_000),
|
||||||
|
minutes: Math.floor((diff % 3_600_000) / 60_000),
|
||||||
|
seconds: Math.floor((diff % 60_000) / 1_000),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
const id = setInterval(tick, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [endDate]);
|
||||||
|
|
||||||
|
const isExpired = !isActive;
|
||||||
|
const isUrgent = countdown.days <= 3;
|
||||||
|
|
||||||
|
const formattedDate = new Date(endDate).toLocaleDateString(undefined, {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-[14px] p-3.5"
|
||||||
|
style={{
|
||||||
|
background: isExpired
|
||||||
|
? 'rgba(255,59,92,0.06)'
|
||||||
|
: isUrgent
|
||||||
|
? 'rgba(255,184,0,0.06)'
|
||||||
|
: g.innerBg,
|
||||||
|
border: isExpired
|
||||||
|
? '1px solid rgba(255,59,92,0.15)'
|
||||||
|
: isUrgent
|
||||||
|
? '1px solid rgba(255,184,0,0.15)'
|
||||||
|
: `1px solid ${g.innerBorder}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||||
|
<div
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||||
|
style={{
|
||||||
|
background: isExpired
|
||||||
|
? 'rgba(255,59,92,0.1)'
|
||||||
|
: isUrgent
|
||||||
|
? 'rgba(255,184,0,0.1)'
|
||||||
|
: g.hoverBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={isExpired ? '#FF3B5C' : isUrgent ? '#FFB800' : g.textSecondary}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||||
|
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{t('dashboard.remaining')}
|
||||||
|
</div>
|
||||||
|
{isExpired ? (
|
||||||
|
<div className="text-[18px] font-bold tracking-tight" style={{ color: '#FF3B5C' }}>
|
||||||
|
{t('subscription.expired')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-baseline gap-1 font-mono tabular-nums">
|
||||||
|
{countdown.days > 0 && (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="text-[20px] font-bold tracking-tight"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
{countdown.days}
|
||||||
|
</span>
|
||||||
|
<span className="mr-1 text-[10px] font-medium text-dark-50/25">
|
||||||
|
{t('subscription.daysShort')}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="text-[20px] font-bold tracking-tight"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
{String(countdown.hours).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[20px] font-bold tracking-tight"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
{String(countdown.minutes).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[20px] font-bold tracking-tight"
|
||||||
|
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||||
|
>
|
||||||
|
{String(countdown.seconds).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-1 text-[10px] font-medium text-dark-50/25">
|
||||||
|
{t('subscription.expiresAt')}: {formattedDate}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default function Subscription() {
|
export default function Subscription() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -61,25 +201,6 @@ export default function Subscription() {
|
|||||||
// Extract subscription from response (null if no subscription)
|
// Extract subscription from response (null if no subscription)
|
||||||
const subscription = subscriptionResponse?.subscription ?? null;
|
const subscription = subscriptionResponse?.subscription ?? null;
|
||||||
|
|
||||||
// Live countdown timer
|
|
||||||
const [countdown, setCountdown] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
|
||||||
useEffect(() => {
|
|
||||||
if (!subscription?.end_date) return;
|
|
||||||
const endTime = new Date(subscription.end_date).getTime();
|
|
||||||
const tick = () => {
|
|
||||||
const diff = Math.max(0, endTime - Date.now());
|
|
||||||
setCountdown({
|
|
||||||
days: Math.floor(diff / 86_400_000),
|
|
||||||
hours: Math.floor((diff % 86_400_000) / 3_600_000),
|
|
||||||
minutes: Math.floor((diff % 3_600_000) / 60_000),
|
|
||||||
seconds: Math.floor((diff % 60_000) / 1_000),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
tick();
|
|
||||||
const id = setInterval(tick, 1000);
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, [subscription?.end_date]);
|
|
||||||
|
|
||||||
// Purchase options (needed for balance_kopeks in device/traffic/server management)
|
// Purchase options (needed for balance_kopeks in device/traffic/server management)
|
||||||
const { data: purchaseOptions } = useQuery({
|
const { data: purchaseOptions } = useQuery({
|
||||||
queryKey: ['purchase-options'],
|
queryKey: ['purchase-options'],
|
||||||
@@ -316,7 +437,6 @@ export default function Subscription() {
|
|||||||
(trafficData?.is_unlimited ?? false) || subscription.traffic_limit_gb === 0;
|
(trafficData?.is_unlimited ?? false) || subscription.traffic_limit_gb === 0;
|
||||||
const zone = getTrafficZone(usedPercent);
|
const zone = getTrafficZone(usedPercent);
|
||||||
const connectedDevices = devicesData?.total ?? 0;
|
const connectedDevices = devicesData?.total ?? 0;
|
||||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -576,7 +696,7 @@ export default function Subscription() {
|
|||||||
{Array.from({ length: subscription.device_limit }, (_, i) => (
|
{Array.from({ length: subscription.device_limit }, (_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="h-[7px] w-[7px] rounded-full transition-all duration-300"
|
className="h-[7px] w-[7px] rounded-full transition-[background-color,box-shadow] duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: i < connectedDevices ? zone.mainHex : g.textGhost,
|
background: i < connectedDevices ? zone.mainHex : g.textGhost,
|
||||||
boxShadow: i < connectedDevices ? `0 0 6px ${zone.mainHex}50` : 'none',
|
boxShadow: i < connectedDevices ? `0 0 6px ${zone.mainHex}50` : 'none',
|
||||||
@@ -601,7 +721,7 @@ export default function Subscription() {
|
|||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
onClick={copyUrl}
|
onClick={copyUrl}
|
||||||
className="flex h-auto items-center rounded-[10px] px-3 transition-all duration-300"
|
className="flex h-auto items-center rounded-[10px] px-3 transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: copied ? 'rgba(62,219,176,0.12)' : g.innerBorder,
|
background: copied ? 'rgba(62,219,176,0.12)' : g.innerBorder,
|
||||||
border: copied ? '1px solid rgba(62,219,176,0.2)' : `1px solid ${g.trackBg}`,
|
border: copied ? '1px solid rgba(62,219,176,0.2)' : `1px solid ${g.trackBg}`,
|
||||||
@@ -616,114 +736,12 @@ export default function Subscription() {
|
|||||||
|
|
||||||
{/* ─── Stats Row ─── */}
|
{/* ─── Stats Row ─── */}
|
||||||
<div className="mb-5 grid grid-cols-2 gap-2.5">
|
<div className="mb-5 grid grid-cols-2 gap-2.5">
|
||||||
{/* Countdown timer */}
|
{/* Countdown timer — isolated to prevent 1s re-renders of entire page */}
|
||||||
{(() => {
|
<CountdownTimer
|
||||||
const isUrgent = countdown.days <= 3;
|
endDate={subscription.end_date}
|
||||||
const isExpired = !subscription.is_active;
|
isActive={subscription.is_active}
|
||||||
return (
|
glassColors={g}
|
||||||
<div
|
/>
|
||||||
className="rounded-[14px] p-3.5"
|
|
||||||
style={{
|
|
||||||
background: isExpired
|
|
||||||
? 'rgba(255,59,92,0.06)'
|
|
||||||
: isUrgent
|
|
||||||
? 'rgba(255,184,0,0.06)'
|
|
||||||
: g.innerBg,
|
|
||||||
border: isExpired
|
|
||||||
? '1px solid rgba(255,59,92,0.15)'
|
|
||||||
: isUrgent
|
|
||||||
? '1px solid rgba(255,184,0,0.15)'
|
|
||||||
: `1px solid ${g.innerBorder}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
|
||||||
<div
|
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
|
||||||
style={{
|
|
||||||
background: isExpired
|
|
||||||
? 'rgba(255,59,92,0.1)'
|
|
||||||
: isUrgent
|
|
||||||
? 'rgba(255,184,0,0.1)'
|
|
||||||
: g.hoverBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="13"
|
|
||||||
height="13"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke={isExpired ? '#FF3B5C' : isUrgent ? '#FFB800' : g.textSecondary}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
||||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{t('dashboard.remaining')}
|
|
||||||
</div>
|
|
||||||
{isExpired ? (
|
|
||||||
<div
|
|
||||||
className="text-[18px] font-bold tracking-tight"
|
|
||||||
style={{ color: '#FF3B5C' }}
|
|
||||||
>
|
|
||||||
{t('subscription.expired')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-baseline gap-1 font-mono tabular-nums">
|
|
||||||
{countdown.days > 0 && (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="text-[20px] font-bold tracking-tight"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
{countdown.days}
|
|
||||||
</span>
|
|
||||||
<span className="mr-1 text-[10px] font-medium text-dark-50/25">
|
|
||||||
{t('subscription.daysShort')}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="text-[20px] font-bold tracking-tight"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
{String(countdown.hours).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="text-[20px] font-bold tracking-tight"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
{String(countdown.minutes).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="text-[20px] font-bold tracking-tight"
|
|
||||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
|
||||||
>
|
|
||||||
{String(countdown.seconds).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-1 text-[10px] font-medium text-dark-50/25">
|
|
||||||
{t('subscription.expiresAt')}: {formattedDate}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Devices */}
|
{/* Devices */}
|
||||||
<div
|
<div
|
||||||
@@ -858,7 +876,7 @@ export default function Subscription() {
|
|||||||
style={{ background: g.trackBg }}
|
style={{ background: g.trackBg }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 rounded-full transition-all duration-500"
|
className="absolute inset-0 rounded-full transition-[width] duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${purchase.progress_percent}%`,
|
width: `${purchase.progress_percent}%`,
|
||||||
background: `linear-gradient(90deg, ${zone.mainHex}, ${zone.mainHex}80)`,
|
background: `linear-gradient(90deg, ${zone.mainHex}, ${zone.mainHex}80)`,
|
||||||
@@ -897,13 +915,13 @@ export default function Subscription() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => autopayMutation.mutate(!subscription.autopay_enabled)}
|
onClick={() => autopayMutation.mutate(!subscription.autopay_enabled)}
|
||||||
disabled={autopayMutation.isPending}
|
disabled={autopayMutation.isPending}
|
||||||
className="relative h-7 w-[52px] rounded-full transition-all duration-300"
|
className="relative h-7 w-[52px] rounded-full transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: subscription.autopay_enabled ? zone.mainHex : g.textGhost,
|
background: subscription.autopay_enabled ? zone.mainHex : g.textGhost,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="absolute top-[3px] h-[22px] w-[22px] rounded-full bg-white transition-all duration-300"
|
className="absolute top-[3px] h-[22px] w-[22px] rounded-full bg-white transition-[left] duration-300"
|
||||||
style={{
|
style={{
|
||||||
left: subscription.autopay_enabled ? '26px' : '3px',
|
left: subscription.autopay_enabled ? '26px' : '3px',
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
||||||
@@ -973,7 +991,7 @@ export default function Subscription() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => pauseMutation.mutate()}
|
onClick={() => pauseMutation.mutate()}
|
||||||
disabled={pauseMutation.isPending}
|
disabled={pauseMutation.isPending}
|
||||||
className="rounded-[10px] px-4 py-2 text-sm font-semibold transition-all duration-300"
|
className="rounded-[10px] px-4 py-2 text-sm font-semibold transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: subscription.is_daily_paused
|
background: subscription.is_daily_paused
|
||||||
? 'rgba(62,219,176,0.12)'
|
? 'rgba(62,219,176,0.12)'
|
||||||
@@ -1084,7 +1102,7 @@ export default function Subscription() {
|
|||||||
style={{ background: g.trackBg }}
|
style={{ background: g.trackBg }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 rounded-full transition-all duration-500"
|
className="absolute inset-0 rounded-full transition-[width] duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
background: 'linear-gradient(90deg, #00E5A0, #00C987)',
|
background: 'linear-gradient(90deg, #00E5A0, #00C987)',
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
animation: border-rotate var(--border-duration, 3s) linear infinite;
|
animation: border-rotate var(--border-duration, 3s) linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover-border-gradient {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.hover-border-gradient:hover,
|
.hover-border-gradient:hover,
|
||||||
.hover-border-gradient:active {
|
.hover-border-gradient:active {
|
||||||
box-shadow: 0 0 20px var(--accent-glow, rgba(var(--color-accent-400), 0.25));
|
box-shadow: 0 0 20px var(--accent-glow, rgba(var(--color-accent-400), 0.25));
|
||||||
@@ -370,12 +374,16 @@ img.twemoji {
|
|||||||
|
|
||||||
.bento-card-hover {
|
.bento-card-hover {
|
||||||
@apply bento-card cursor-pointer;
|
@apply bento-card cursor-pointer;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition:
|
||||||
|
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bento-card-hover:hover {
|
.bento-card-hover:hover {
|
||||||
@apply border-dark-600/50 bg-dark-800/60 shadow-lg;
|
@apply border-dark-600/50 bg-dark-800/60 shadow-lg;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-2px);
|
||||||
/* Intensify Glass Border & Add Top Spotlight */
|
/* Intensify Glass Border & Add Top Spotlight */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.1),
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.1),
|
||||||
@@ -405,12 +413,15 @@ img.twemoji {
|
|||||||
|
|
||||||
.bento-card-glow {
|
.bento-card-glow {
|
||||||
@apply bento-card cursor-pointer;
|
@apply bento-card cursor-pointer;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition:
|
||||||
|
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bento-card-glow:hover {
|
.bento-card-glow:hover {
|
||||||
@apply border-accent-500/30 shadow-glow;
|
@apply border-accent-500/30 shadow-glow;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bento-card-glow:active {
|
.bento-card-glow:active {
|
||||||
@@ -451,7 +462,7 @@ img.twemoji {
|
|||||||
|
|
||||||
.light .bento-card-hover:hover {
|
.light .bento-card-hover:hover {
|
||||||
@apply border-champagne-400/50 bg-champagne-50 shadow-md;
|
@apply border-champagne-400/50 bg-champagne-50 shadow-md;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-2px);
|
||||||
/* Intensify Light Theme Glass Border */
|
/* Intensify Light Theme Glass Border */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 0 rgba(0, 0, 0, 0.06),
|
inset 0 1px 0 0 rgba(0, 0, 0, 0.06),
|
||||||
@@ -465,14 +476,14 @@ img.twemoji {
|
|||||||
|
|
||||||
.light .bento-card-glow:hover {
|
.light .bento-card-glow:hover {
|
||||||
@apply border-accent-400/40 shadow-lg;
|
@apply border-accent-400/40 shadow-lg;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== DARK THEME COMPONENTS (default) ========== */
|
/* ========== DARK THEME COMPONENTS (default) ========== */
|
||||||
|
|
||||||
/* Cards - Dark (unified style) */
|
/* Cards - Dark (unified style) */
|
||||||
.card {
|
.card {
|
||||||
@apply border border-dark-700/40 bg-dark-900/70 p-5 transition-all duration-200 sm:p-6;
|
@apply border border-dark-700/40 bg-dark-900/70 p-5 transition-colors duration-200 sm:p-6;
|
||||||
border-radius: var(--bento-radius);
|
border-radius: var(--bento-radius);
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
|
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
|
||||||
|
|||||||
Reference in New Issue
Block a user