refactor(tokens): extract subscription urgent + critical semantic colors

The #FFB800 (23 hits) and #FF3B5C (11 hits) raw hex literals across
Subscription / SubscriptionPurchase / TrialOfferCard / useTrafficZone
all represent the same semantic: 'expiring soon' and 'expired'.
Distinct from warning-500 (#f59e0b) and error-500 (#ef4444) — the
subscription timeline needs a sharper, hotter signal than the
system-wide warning/error roles.

Add two semantic tokens:
  --color-urgent-400:   255, 184, 0    (#FFB800)
  --color-critical-500: 255, 59, 92    (#FF3B5C)

Exposed via Tailwind as urgent.400 / critical.500. Sweep all
single-and double-quoted hex literals to rgb(var(--color-...))
form so style attrs, SVG stroke=, and ternary string values all
resolve through CSS vars (auto-respects future light-theme
variants).

Left as-is: 4 hex appearances inside gradient pair strings —
linear-gradient(135deg, #FFB800, #FF8C00) — where the partner
color (#FF8C00 / #FF6B35) isn't in the token set. Partial-replace
inside gradient strings would just add noise; full extraction of
the orange-ramp companion is out of scope for this pass.
This commit is contained in:
c0mrade
2026-05-26 20:20:05 +03:00
parent eaaf5cfebb
commit cef675825b
8 changed files with 67 additions and 29 deletions

View File

@@ -107,14 +107,14 @@ export default function SubscriptionCardExpired({
r: 255, r: 255,
g: 184, g: 184,
b: 0, b: 0,
hex: '#FFB800', hex: 'rgb(var(--color-urgent-400))',
gradient: 'linear-gradient(135deg, #FFB800, #FF8C00)', gradient: 'linear-gradient(135deg, #FFB800, #FF8C00)',
} }
: { : {
r: 255, r: 255,
g: 59, g: 59,
b: 92, b: 92,
hex: '#FF3B5C', hex: 'rgb(var(--color-critical-500))',
gradient: 'linear-gradient(135deg, #FF3B5C, #FF6B35)', gradient: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
}; };

View File

@@ -115,7 +115,7 @@ export default function TrialOfferCard({
height="26" height="26"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="#FFB800" stroke="rgb(var(--color-urgent-400))"
strokeWidth="1.5" strokeWidth="1.5"
aria-hidden="true" aria-hidden="true"
> >
@@ -157,11 +157,14 @@ export default function TrialOfferCard({
> >
<span <span
className="text-[32px] font-extrabold leading-none tracking-tight" className="text-[32px] font-extrabold leading-none tracking-tight"
style={{ color: '#FFB800' }} style={{ color: 'rgb(var(--color-urgent-400))' }}
> >
{trialInfo.price_rubles.toFixed(0)} {trialInfo.price_rubles.toFixed(0)}
</span> </span>
<span className="text-base font-semibold opacity-70" style={{ color: '#FFB800' }}> <span
className="text-base font-semibold opacity-70"
style={{ color: 'rgb(var(--color-urgent-400))' }}
>
{currencySymbol} {currencySymbol}
</span> </span>
</div> </div>

View File

@@ -24,7 +24,7 @@ export default function PurchaseCTAButton({
// Daily tariffs renew automatically — no manual renewal button needed in multi-tariff // Daily tariffs renew automatically — no manual renewal button needed in multi-tariff
if (isMultiTariff && isDaily && !isExpired) return null; if (isMultiTariff && isDaily && !isExpired) return null;
const accentColor = isExpired ? '#FF3B5C' : 'rgb(var(--color-accent-400))'; const accentColor = isExpired ? 'rgb(var(--color-critical-500))' : 'rgb(var(--color-accent-400))';
const buttonText = isExpired const buttonText = isExpired
? t('subscription.getSubscription') ? t('subscription.getSubscription')

View File

@@ -5,8 +5,8 @@ import type { ThemeColors } from '../types/theme';
const FALLBACKS: Record<TrafficColorKey, string> = { const FALLBACKS: Record<TrafficColorKey, string> = {
accent: '#3b82f6', accent: '#3b82f6',
warning: '#FFB800', warning: 'rgb(var(--color-urgent-400))',
error: '#FF3B5C', error: 'rgb(var(--color-critical-500))',
}; };
const COLOR_MAP: Record<TrafficColorKey, keyof ThemeColors> = { const COLOR_MAP: Record<TrafficColorKey, keyof ThemeColors> = {

View File

@@ -98,7 +98,13 @@ const CountdownTimer = memo(function CountdownTimer({
height="13" height="13"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke={isExpired ? '#FF3B5C' : isUrgent ? '#FFB800' : g.textSecondary} stroke={
isExpired
? 'rgb(var(--color-critical-500))'
: isUrgent
? 'rgb(var(--color-urgent-400))'
: g.textSecondary
}
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -111,7 +117,10 @@ const CountdownTimer = memo(function CountdownTimer({
{t('dashboard.remaining')} {t('dashboard.remaining')}
</div> </div>
{isExpired ? ( {isExpired ? (
<div className="text-[18px] font-bold tracking-tight" style={{ color: '#FF3B5C' }}> <div
className="text-[18px] font-bold tracking-tight"
style={{ color: 'rgb(var(--color-critical-500))' }}
>
{t('subscription.expired')} {t('subscription.expired')}
</div> </div>
) : ( ) : (
@@ -121,7 +130,7 @@ const CountdownTimer = memo(function CountdownTimer({
<> <>
<span <span
className="text-[20px] font-bold tracking-tight" className="text-[20px] font-bold tracking-tight"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
{countdown.days} {countdown.days}
</span> </span>
@@ -132,31 +141,31 @@ const CountdownTimer = memo(function CountdownTimer({
)} )}
<span <span
className="text-[20px] font-bold tracking-tight" className="text-[20px] font-bold tracking-tight"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
{String(countdown.hours).padStart(2, '0')} {String(countdown.hours).padStart(2, '0')}
</span> </span>
<span <span
className="mx-[-1px] text-[16px] font-bold opacity-30" className="mx-[-1px] text-[16px] font-bold opacity-30"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
: :
</span> </span>
<span <span
className="text-[20px] font-bold tracking-tight" className="text-[20px] font-bold tracking-tight"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
{String(countdown.minutes).padStart(2, '0')} {String(countdown.minutes).padStart(2, '0')}
</span> </span>
<span <span
className="mx-[-1px] text-[16px] font-bold opacity-30" className="mx-[-1px] text-[16px] font-bold opacity-30"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
: :
</span> </span>
<span <span
className="text-[20px] font-bold tracking-tight" className="text-[20px] font-bold tracking-tight"
style={{ color: isUrgent ? '#FFB800' : g.text }} style={{ color: isUrgent ? 'rgb(var(--color-urgent-400))' : g.text }}
> >
{String(countdown.seconds).padStart(2, '0')} {String(countdown.seconds).padStart(2, '0')}
</span> </span>
@@ -710,8 +719,8 @@ export default function Subscription() {
color: subscription.is_active color: subscription.is_active
? zone.mainHex ? zone.mainHex
: subscription.is_limited : subscription.is_limited
? '#FFB800' ? 'rgb(var(--color-urgent-400))'
: '#FF3B5C', : 'rgb(var(--color-critical-500))',
}} }}
> >
{subscription.is_active {subscription.is_active
@@ -746,7 +755,7 @@ export default function Subscription() {
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="#FFB800" stroke="rgb(var(--color-urgent-400))"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -758,7 +767,10 @@ export default function Subscription() {
</svg> </svg>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-semibold" style={{ color: '#FFB800' }}> <p
className="text-sm font-semibold"
style={{ color: 'rgb(var(--color-urgent-400))' }}
>
{t('subscription.trafficLimitedTitle')} {t('subscription.trafficLimitedTitle')}
</p> </p>
<p className="mt-1 text-xs text-dark-400"> <p className="mt-1 text-xs text-dark-400">
@@ -1269,7 +1281,7 @@ export default function Subscription() {
color: color:
subscription.is_daily_paused || subscription.status === 'disabled' subscription.is_daily_paused || subscription.status === 'disabled'
? 'rgb(var(--color-accent-400))' ? 'rgb(var(--color-accent-400))'
: '#FFB800', : 'rgb(var(--color-urgent-400))',
}} }}
> >
{pauseMutation.isPending ? ( {pauseMutation.isPending ? (
@@ -1306,7 +1318,7 @@ export default function Subscription() {
style={{ style={{
background: 'rgba(255,59,92,0.08)', background: 'rgba(255,59,92,0.08)',
border: '1px solid rgba(255,59,92,0.15)', border: '1px solid rgba(255,59,92,0.15)',
color: '#FF3B5C', color: 'rgb(var(--color-critical-500))',
}} }}
> >
{getErrorMessage(pauseMutation.error)} {getErrorMessage(pauseMutation.error)}
@@ -1324,11 +1336,14 @@ export default function Subscription() {
}} }}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-lg" style={{ color: '#FFB800' }}> <div className="text-lg" style={{ color: 'rgb(var(--color-urgent-400))' }}>
</div> </div>
<div> <div>
<div className="text-sm font-semibold" style={{ color: '#FFB800' }}> <div
className="text-sm font-semibold"
style={{ color: 'rgb(var(--color-urgent-400))' }}
>
{t('subscription.pause.pausedInfo')} {t('subscription.pause.pausedInfo')}
</div> </div>
<div className="mt-1 text-[12px] text-dark-50/35"> <div className="mt-1 text-[12px] text-dark-50/35">
@@ -2412,7 +2427,7 @@ export default function Subscription() {
}} }}
disabled={deleteAllDevicesMutation.isPending} disabled={deleteAllDevicesMutation.isPending}
className="text-[11px] font-medium transition-colors" className="text-[11px] font-medium transition-colors"
style={{ color: '#FF3B5C' }} style={{ color: 'rgb(var(--color-critical-500))' }}
> >
{t('subscription.deleteAllDevices')} {t('subscription.deleteAllDevices')}
</button> </button>

View File

@@ -477,7 +477,7 @@ export default function SubscriptionPurchase() {
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="#FFB800" stroke="rgb(var(--color-urgent-400))"
strokeWidth="1.5" strokeWidth="1.5"
aria-hidden="true" aria-hidden="true"
> >
@@ -489,7 +489,10 @@ export default function SubscriptionPurchase() {
</svg> </svg>
</div> </div>
<div> <div>
<div className="text-sm font-semibold" style={{ color: '#FFB800' }}> <div
className="text-sm font-semibold"
style={{ color: 'rgb(var(--color-urgent-400))' }}
>
{t('subscription.trialUpgrade.title')} {t('subscription.trialUpgrade.title')}
</div> </div>
<div className="mt-1 text-[12px] text-dark-50/40"> <div className="mt-1 text-[12px] text-dark-50/40">
@@ -522,7 +525,7 @@ export default function SubscriptionPurchase() {
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="#FF3B5C" stroke="rgb(var(--color-critical-500))"
strokeWidth="1.5" strokeWidth="1.5"
aria-hidden="true" aria-hidden="true"
> >
@@ -534,7 +537,10 @@ export default function SubscriptionPurchase() {
</svg> </svg>
</div> </div>
<div> <div>
<div className="text-sm font-semibold" style={{ color: '#FF3B5C' }}> <div
className="text-sm font-semibold"
style={{ color: 'rgb(var(--color-critical-500))' }}
>
{t('subscription.expiredBanner.title')} {t('subscription.expiredBanner.title')}
</div> </div>
<div className="mt-1 text-[12px] text-dark-50/40"> <div className="mt-1 text-[12px] text-dark-50/40">

View File

@@ -180,6 +180,13 @@
--color-error-900: 127, 29, 29; --color-error-900: 127, 29, 29;
--color-error-950: 69, 10, 10; --color-error-950: 69, 10, 10;
/* Subscription-status semantic tokens — distinct from warning/error
because the urgency timeline ("expiring soon" vs "expired") needs a
sharper, hotter signal than the system-wide warning/error roles.
Extracted from inline #FFB800 / #FF3B5C in Subscription.tsx etc. */
--color-urgent-400: 255, 184, 0; /* #FFB800 — expiring soon */
--color-critical-500: 255, 59, 92; /* #FF3B5C — expired */
/* Theme semantic colors (hex for direct use) */ /* Theme semantic colors (hex for direct use) */
--color-dark-bg: #0a0f1a; --color-dark-bg: #0a0f1a;
--color-dark-surface: #0f172a; --color-dark-surface: #0f172a;

View File

@@ -101,6 +101,13 @@ export default {
900: withOpacity('--color-error-900'), 900: withOpacity('--color-error-900'),
950: withOpacity('--color-error-950'), 950: withOpacity('--color-error-950'),
}, },
// Subscription-status semantic tokens — see globals.css for rationale.
urgent: {
400: withOpacity('--color-urgent-400'),
},
critical: {
500: withOpacity('--color-critical-500'),
},
}, },
fontFamily: { fontFamily: {
sans: [ sans: [