fix(theme): readable text on any operator palette + admin consistency pass

Контраст-аудит (WCAG-обход всех текстовых узлов, 4 палитры × юзер+админ
страницы) показал системные провалы читаемости: вторичный текст 1.5-2.5
на светлой/кастомных темах, статусные цвета 1.3-1.6, белый текст на
светлых акцентах 1.9.

Корневые фиксы:
- useThemeColors: контраст-кламп производных серых токенов — dark-400/
  champagne-600 (вторичный текст) держат >=5.0 к поверхности, dark-500/
  champagne-500 (подсказки) >=3.8; палитры с достаточным контрастом
  рендерятся байт-в-байт как раньше
- --color-on-accent/-success/-warning/-error: чёрный или белый текст
  по фактической яркости цвета оператора; text-white на accent-заливках
  заменён на text-on-accent (135 мест + variant примитива Button +
  .btn-primary)
- .light: статусные текстовые оттенки *-300/400 ремапятся на тёмные
  *-700 (жёлтая «Админка» 1.26 -> 3.8+, зелёные суммы 1.4 -> 3.4+,
  бейджи состояний)

Замер после: операторская тёмная палитра — 0 нарушений (всё >=4.5),
худшие точки остальных подняты с 1.26-1.6 до 3.2+.

Консистентность:
- заголовки админ-страниц приведены к text-xl font-bold text-dark-100
  (были 18/20/24/30px и 600/700 вперемешку)
- «Создать FAQ»: warning-500/80+белый -> warning-500+text-on-warning
- formatTraffic: единицы из i18n (убран микс «GB»/«ГБ» на одном экране)
- categoryCanvas -> categoryCANVAS в локалях (битый ключ на настройках)
- «Управление сквадами» -> «Управление локациями» (юзерский экран)
- мин-макс сумм на выборе метода: dark-600 -> dark-400
- неактивные пункты нижнего нава: dark-500 -> dark-400
This commit is contained in:
c0mrade
2026-06-11 15:49:17 +03:00
parent 47119c4c1f
commit 7f68e2cd5e
91 changed files with 287 additions and 176 deletions

View File

@@ -74,7 +74,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600"
>
Reload page
</button>
@@ -110,7 +110,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{isChunk ? 'Reload' : 'Try again'}
</button>

View File

@@ -281,7 +281,7 @@ export default function SuccessNotificationModal() {
{isSubscription && (
<button
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-on-accent shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
>
<RocketIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
@@ -301,7 +301,7 @@ export default function SuccessNotificationModal() {
{isDevicesPurchased && (
<button
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-on-accent shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
>
<DevicesIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>

View File

@@ -114,7 +114,7 @@ export function AnalyticsTab() {
<button
onClick={handleSaveYandex}
disabled={updateMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<CheckIcon />
</button>
@@ -253,7 +253,7 @@ export function AnalyticsTab() {
<button
onClick={handleSaveGoogleId}
disabled={updateMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<CheckIcon />
</button>
@@ -307,7 +307,7 @@ export function AnalyticsTab() {
<button
onClick={handleSaveGoogleLabel}
disabled={updateMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<CheckIcon />
</button>

View File

@@ -68,7 +68,7 @@ export function BackgroundEditor() {
'w-full rounded-xl py-3 text-sm font-medium transition-colors',
saveStatus === 'saved'
? 'bg-success-500/20 text-success-400'
: 'bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50',
: 'bg-accent-500 text-on-accent hover:bg-accent-600 disabled:opacity-50',
)}
>
{saveStatus === 'saving'

View File

@@ -168,7 +168,7 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) {
<button
onClick={() => updateBrandingMutation.mutate(newName)}
disabled={updateBrandingMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<CheckIcon />
</button>

View File

@@ -228,7 +228,7 @@ export function ButtonsTab() {
? 'bg-success-500 text-white'
: cfg.style === 'danger'
? 'bg-error-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
}`}
>
{t(`admin.buttons.styles.${cfg.style}`)}
@@ -321,7 +321,7 @@ export function ButtonsTab() {
<button
onClick={handleSave}
disabled={updateMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{updateMutation.isPending ? t('common.saving') : t('common.save')}
</button>

View File

@@ -346,7 +346,7 @@ export function ColoredItemCombobox({
type="button"
onClick={handleCreate}
disabled={isCreating}
className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{isCreating ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />

View File

@@ -72,7 +72,7 @@ function MaxPerRowSelector({ value, onChange }: MaxPerRowSelectorProps) {
onClick={() => onChange(n)}
className={`flex h-7 w-7 items-center justify-center rounded-lg text-xs font-semibold transition-all ${
value === n
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700/50 text-dark-400 hover:bg-dark-600 hover:text-dark-300'
}`}
>
@@ -788,7 +788,7 @@ export function MenuEditorTab() {
<button
onClick={handleSave}
disabled={updateMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{updateMutation.isPending ? t('common.saving') : t('common.save')}
</button>

View File

@@ -122,7 +122,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
</button>
<button
onClick={handleSave}
className="flex items-center gap-1.5 rounded-lg bg-accent-500 px-3 py-1.5 text-sm text-white transition-colors hover:bg-accent-600"
className="flex items-center gap-1.5 rounded-lg bg-accent-500 px-3 py-1.5 text-sm text-on-accent transition-colors hover:bg-accent-600"
>
<CheckIcon />
{t('admin.settings.saveButton')}
@@ -159,7 +159,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
/>
<button
onClick={handleSave}
className="rounded-lg bg-accent-500 p-2 text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 p-2 text-on-accent transition-colors hover:bg-accent-600"
title={t('admin.settings.saveHint')}
>
<CheckIcon />

View File

@@ -215,7 +215,7 @@ export function SortableSelectedMethodCard({
className={cn(
'flex h-3.5 w-3.5 items-center justify-center rounded',
enabled
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'border border-dark-600 bg-dark-700',
)}
>

View File

@@ -428,7 +428,7 @@ export function ThemeTab() {
<button
onClick={() => updateColorsMutation.mutate(draftColors)}
disabled={updateColorsMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{updateColorsMutation.isPending
? t('common.saving', t('common.save'))

View File

@@ -621,7 +621,7 @@ export function ActionModal({
<button
onClick={onClose}
className="w-full rounded-xl bg-accent-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="w-full rounded-xl bg-accent-500 px-4 py-2.5 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.close')}
</button>
@@ -647,7 +647,7 @@ export function ActionModal({
<button
onClick={handleSubmit}
disabled={isConfirmDisabled}
className="flex min-h-[44px] flex-1 items-center justify-center gap-2 rounded-xl bg-accent-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex min-h-[44px] flex-1 items-center justify-center gap-2 rounded-xl bg-accent-500 px-4 py-2.5 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{t('admin.bulkActions.confirm')}
</button>

View File

@@ -208,7 +208,7 @@ export function FloatingActionBar({
<div className="relative ml-auto">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2 rounded-xl bg-accent-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="flex items-center gap-2 rounded-xl bg-accent-500 px-4 py-2 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.actions')}
<ChevronDownIcon />

View File

@@ -52,7 +52,7 @@ export function CountryFilter({
>
<GlobeIcon className="h-4 w-4" />
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-on-accent">
{activeCount}
</span>
)}

View File

@@ -56,7 +56,7 @@ export function NodeFilter({
<ServerIcon className="h-4 w-4" />
{t('admin.trafficUsage.nodes')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-on-accent">
{activeCount}
</span>
)}

View File

@@ -79,7 +79,7 @@ export function PeriodSelector({
onClick={() => onChange(p)}
className={`rounded-lg px-2.5 py-1 text-xs font-medium transition-colors ${
value === p
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-400 hover:bg-dark-700 hover:text-dark-200'
}`}
>

View File

@@ -67,7 +67,7 @@ export function StatusFilter({
<StatusIcon className="h-4 w-4" />
{t('admin.trafficUsage.status')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-on-accent">
{activeCount}
</span>
)}

View File

@@ -54,7 +54,7 @@ export function TariffFilter({
<FilterIcon className="h-4 w-4" />
{t('admin.trafficUsage.tariff')}
{activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-on-accent">
{activeCount}
</span>
)}

View File

@@ -378,7 +378,7 @@ export function InfoTab(props: InfoTabProps) {
<button
onClick={onUpdateReferralCommission}
disabled={actionLoading}
className="w-full rounded-lg bg-accent-500 px-2 py-1 text-xs text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="w-full rounded-lg bg-accent-500 px-2 py-1 text-xs text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{actionLoading ? t('common.loading') : t('common.save')}
</button>
@@ -475,7 +475,7 @@ export function InfoTab(props: InfoTabProps) {
disabled={actionLoading}
className={`rounded-lg px-3 py-2 text-sm font-medium transition-all disabled:opacity-50 ${
confirmingAction === 'resetTrial'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-accent-500/15 text-accent-400 hover:bg-accent-500/25'
}`}
>

View File

@@ -464,7 +464,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
<button
onClick={() => selectedTrafficGb && onAddTraffic(Number(selectedTrafficGb))}
disabled={actionLoading || !selectedTrafficGb}
className="shrink-0 rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="shrink-0 rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{t('admin.users.detail.subscription.addButton')}
</button>

View File

@@ -348,7 +348,7 @@ function ChatView({
onClick={onReply}
disabled={!replyText.trim() || replySending}
aria-label={t('admin.tickets.sendReply', 'Send reply')}
className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0"
className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0"
>
{replySending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />

View File

@@ -57,7 +57,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro
{/* Activate button */}
<Link
to={`/gift?tab=activate&code=${gift.token}`}
className="shrink-0 rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="shrink-0 rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.pending.activate')}
</Link>

View File

@@ -80,7 +80,7 @@ export function MobileBottomNav({
onClick={handleNavClick}
className={cn(
'relative flex min-w-[56px] flex-1 shrink-0 flex-col items-center justify-center rounded-2xl px-3 py-2.5 transition-all duration-200',
isActive(item.path) ? 'text-accent-400' : 'text-dark-500 hover:text-dark-300',
isActive(item.path) ? 'text-accent-400' : 'text-dark-400 hover:text-dark-200',
)}
>
{isActive(item.path) && (

View File

@@ -13,7 +13,7 @@ export const buttonVariants = cva(
variants: {
variant: {
primary: [
'bg-accent-500 text-white',
'bg-accent-500 text-on-accent',
'hover:bg-accent-600',
'active:bg-accent-700',
'shadow-linear-sm hover:shadow-linear',

View File

@@ -107,7 +107,7 @@ export function TariffPickerGrid({
</p>
<button
onClick={() => navigate('/subscriptions')}
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('subscription.backToList', 'Мои подписки')}
</button>

View File

@@ -54,6 +54,61 @@ function interpolateRgb(
);
}
type Rgb = { r: number; g: number; b: number };
function mixRgb(rgb1: Rgb, rgb2: Rgb, factor: number): Rgb {
return {
r: Math.round(rgb1.r + (rgb2.r - rgb1.r) * factor),
g: Math.round(rgb1.g + (rgb2.g - rgb1.g) * factor),
b: Math.round(rgb1.b + (rgb2.b - rgb1.b) * factor),
};
}
// WCAG relative luminance
function relativeLuminance({ r, g, b }: Rgb): number {
const srgb = (v: number) => {
const c = v / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
}
// WCAG contrast ratio between two colors
function contrastRatio(a: Rgb, b: Rgb): number {
const l1 = relativeLuminance(a);
const l2 = relativeLuminance(b);
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
/**
* Guarantee a minimum contrast for a TEXT color against its background.
*
* Operators pick arbitrary palette colors in the theme editor, and several
* tokens are additionally blended toward the surface (dark-500 = secondary
* text mixed 40% into the card color). Without a floor, hint/meta text
* regularly lands at a 1.5-2.5 contrast ratio and becomes unreadable.
* When the color already passes, it is returned untouched, so well-tuned
* palettes render byte-for-byte the same as before.
*/
function ensureReadable(fg: Rgb, towards: Rgb, bg: Rgb, minRatio: number): Rgb {
if (contrastRatio(fg, bg) >= minRatio) return fg;
for (let t = 0.1; t <= 1; t += 0.1) {
const mixed = mixRgb(fg, towards, t);
if (contrastRatio(mixed, bg) >= minRatio) return mixed;
}
return towards;
}
// Black-or-white text for a given button/badge background: pick whichever
// side actually reads on top of it (operators may choose a light accent).
function onColorFor(bgTriplet: string): string {
const [r, g, b] = bgTriplet.split(',').map((x) => Number(x.trim()));
const bg = { r, g, b };
const white = { r: 255, g: 255, b: 255 };
const ink = { r: 15, g: 23, b: 42 };
return contrastRatio(white, bg) >= contrastRatio(ink, bg) ? '255, 255, 255' : '15, 23, 42';
}
// Apply theme colors as CSS variables (RGB format for Tailwind opacity support)
export function applyThemeColors(colors: ThemeColors): void {
const root = document.documentElement;
@@ -70,6 +125,17 @@ export function applyThemeColors(colors: ThemeColors): void {
const darkTextRgb = hexToRgb(colors.darkText);
const darkTextSecRgb = hexToRgb(colors.darkTextSecondary);
// Contrast floors: secondary text must stay readable on the card surface
// regardless of the operator-chosen palette (AA 4.5 for dark-400, a softer
// 3.5 floor for the blended hint token dark-500).
const darkTextSecReadable = ensureReadable(darkTextSecRgb, darkTextRgb, darkSurfaceRgb, 5.0);
const darkHintReadable = ensureReadable(
mixRgb(darkTextSecRgb, darkSurfaceRgb, 0.4),
darkTextRgb,
darkSurfaceRgb,
3.8,
);
// Apply dark palette with actual user colors:
// Text colors (light shades): 50-100 = primary text, 200-300 = mixed, 400 = secondary text
root.style.setProperty(
@@ -84,11 +150,14 @@ export function applyThemeColors(colors: ThemeColors): void {
root.style.setProperty('--color-dark-300', interpolateRgb(darkTextRgb, darkTextSecRgb, 0.66));
root.style.setProperty(
'--color-dark-400',
rgbToString(darkTextSecRgb.r, darkTextSecRgb.g, darkTextSecRgb.b),
rgbToString(darkTextSecReadable.r, darkTextSecReadable.g, darkTextSecReadable.b),
);
// Transition colors (500-700): interpolate between secondary text and surface
root.style.setProperty('--color-dark-500', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.4));
root.style.setProperty(
'--color-dark-500',
rgbToString(darkHintReadable.r, darkHintReadable.g, darkHintReadable.b),
);
root.style.setProperty('--color-dark-600', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.6));
root.style.setProperty('--color-dark-700', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.8));
@@ -122,11 +191,23 @@ export function applyThemeColors(colors: ThemeColors): void {
root.style.setProperty('--color-champagne-300', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.2));
root.style.setProperty('--color-champagne-400', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.4));
// Transition colors (500-600): between bg and text
root.style.setProperty('--color-champagne-500', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.6));
// Transition colors (500-600): between bg and text.
// Same contrast floors as the dark palette: champagne-600 backs dark-400
// (secondary text) in the light theme, champagne-500 backs dark-500 (hints).
const lightHintReadable = ensureReadable(
mixRgb(lightBgRgb, lightTextSecRgb, 0.6),
lightTextRgb,
lightSurfaceRgb,
3.8,
);
const lightTextSecReadable = ensureReadable(lightTextSecRgb, lightTextRgb, lightSurfaceRgb, 5.0);
root.style.setProperty(
'--color-champagne-500',
rgbToString(lightHintReadable.r, lightHintReadable.g, lightHintReadable.b),
);
root.style.setProperty(
'--color-champagne-600',
rgbToString(lightTextSecRgb.r, lightTextSecRgb.g, lightTextSecRgb.b),
rgbToString(lightTextSecReadable.r, lightTextSecReadable.g, lightTextSecReadable.b),
);
// Text colors (700-950): secondary to primary text
@@ -154,6 +235,13 @@ export function applyThemeColors(colors: ThemeColors): void {
root.style.setProperty(`--color-error-${shade}`, errorPalette[shade]);
}
// Readable text color on top of each status color (buttons, filled badges).
// Hardcoded white breaks the moment an operator picks a light accent.
root.style.setProperty('--color-on-accent', onColorFor(accentPalette[500]));
root.style.setProperty('--color-on-success', onColorFor(successPalette[500]));
root.style.setProperty('--color-on-warning', onColorFor(warningPalette[500]));
root.style.setProperty('--color-on-error', onColorFor(errorPalette[500]));
// Apply semantic colors (hex for direct use)
root.style.setProperty('--color-dark-bg', colors.darkBackground);
root.style.setProperty('--color-dark-surface', colors.darkSurface);

View File

@@ -25,10 +25,9 @@
"copy": "Copy",
"all": "All",
"units": {
"tb": "TB",
"gb": "GB",
"perGb": "/GB",
"days": "days",
"mo": "mo"
"mb": "MB"
},
"retry": "Retry",
"saving": "Saving...",
@@ -1119,7 +1118,7 @@
"globalOpacity": "Opacity",
"globalBlur": "Blur",
"categoryCSS": "CSS",
"categoryCanvas": "Canvas",
"categoryCANVAS": "Canvas",
"categorySVG": "SVG",
"aurora": "Aurora",
"auroraDesc": "Northern lights with smooth gradients",

View File

@@ -22,10 +22,9 @@
"currency": "تومان",
"copy": "کپی",
"units": {
"tb": "TB",
"gb": "GB",
"perGb": "/GB",
"days": "روز",
"mo": "ماه"
"mb": "MB"
},
"add": "افزودن",
"refresh": "بازخوانی",
@@ -1001,7 +1000,7 @@
"globalOpacity": "شفافیت",
"globalBlur": "تاری",
"categoryCSS": "CSS",
"categoryCanvas": "Canvas",
"categoryCANVAS": "Canvas",
"categorySVG": "SVG",
"aurora": "شفق قطبی",
"auroraDesc": "شفق شمالی با گرادیان‌های نرم",

View File

@@ -28,10 +28,9 @@
"saving": "Сохранение...",
"understand": "Понятно",
"units": {
"tb": "ТБ",
"gb": "ГБ",
"perGb": "Б",
"days": "дн",
"mo": "мес"
"mb": "МБ"
},
"balance": "Баланс",
"copied": "Скопировано",
@@ -613,8 +612,8 @@
"unlimited": "Безлимит",
"buyTrafficGb": "Купить {{gb}} ГБ",
"buyUnlimited": "Купить Безлимит",
"manageServers": "Управление сквадами",
"manageServersTitle": "Управление сквадами",
"manageServers": "Управление локациями",
"manageServersTitle": "Управление локациями",
"connectedDevices": "Подключённые устройства",
"currentDeviceLimit": "Текущий лимит устройств",
"decrementDevices": "Уменьшить кол-во устройств",
@@ -1149,7 +1148,7 @@
"globalOpacity": "Прозрачность",
"globalBlur": "Размытие",
"categoryCSS": "CSS",
"categoryCanvas": "Canvas",
"categoryCANVAS": "Canvas",
"categorySVG": "SVG",
"aurora": "Aurora",
"auroraDesc": "Северное сияние с плавными переливами",

View File

@@ -28,10 +28,9 @@
"understand": "明白了",
"collapse": "收起",
"units": {
"tb": "TB",
"gb": "GB",
"perGb": "/GB",
"days": "天",
"mo": "月"
"mb": "MB"
},
"balance": "余额",
"copied": "已复制",
@@ -1001,7 +1000,7 @@
"globalOpacity": "透明度",
"globalBlur": "模糊",
"categoryCSS": "CSS",
"categoryCanvas": "Canvas",
"categoryCANVAS": "Canvas",
"categorySVG": "SVG",
"aurora": "极光",
"auroraDesc": "柔和渐变的北极光",

View File

@@ -545,7 +545,7 @@ export default function AdminAuditLog() {
handleExport();
}}
disabled={exporting}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<DownloadIcon />
<span className="hidden sm:inline">
@@ -614,7 +614,7 @@ export default function AdminAuditLog() {
<span
className={`flex h-4 w-4 items-center justify-center rounded border text-xs ${
isSelected
? 'border-accent-500 bg-accent-500 text-white'
? 'border-accent-500 bg-accent-500 text-on-accent'
: 'border-dark-500 bg-dark-800'
}`}
>
@@ -734,7 +734,7 @@ export default function AdminAuditLog() {
<div className="mt-4 flex items-center gap-3">
<button
onClick={handleApplyFilters}
className="rounded-lg bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 px-4 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('admin.auditLog.filters.apply')}
</button>

View File

@@ -702,7 +702,7 @@ export default function AdminBroadcastCreate() {
onClick={() => toggleButton(button.key)}
className={`rounded-lg px-3 py-2 text-sm transition-colors ${
selectedButtons.includes(button.key)
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
@@ -770,7 +770,7 @@ export default function AdminBroadcastCreate() {
onClick={() => setNewButtonActionType('callback')}
className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${
newButtonActionType === 'callback'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
@@ -781,7 +781,7 @@ export default function AdminBroadcastCreate() {
onClick={() => setNewButtonActionType('url')}
className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${
newButtonActionType === 'url'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>

View File

@@ -155,7 +155,7 @@ export default function AdminBroadcastDetail() {
<p className="text-dark-400">{t('admin.broadcasts.notFound')}</p>
<button
onClick={() => navigate('/admin/broadcasts')}
className="rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.back')}
</button>

View File

@@ -117,7 +117,7 @@ export default function AdminBroadcasts() {
</button>
<button
onClick={() => navigate('/admin/broadcasts/create')}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
<span className="hidden sm:inline">{t('admin.broadcasts.create')}</span>

View File

@@ -79,7 +79,9 @@ function ServerSelector({
>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${
selected.includes(server.squad_uuid) ? 'bg-accent-500 text-white' : 'bg-dark-600'
selected.includes(server.squad_uuid)
? 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>
{selected.includes(server.squad_uuid) && <CheckIcon />}

View File

@@ -79,7 +79,9 @@ function ServerSelector({
>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${
selected.includes(server.squad_uuid) ? 'bg-accent-500 text-white' : 'bg-dark-600'
selected.includes(server.squad_uuid)
? 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>
{selected.includes(server.squad_uuid) && <CheckIcon />}

View File

@@ -136,7 +136,7 @@ export default function AdminCampaigns() {
</div>
<button
onClick={() => navigate('/admin/campaigns/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.campaigns.createButton')}

View File

@@ -424,7 +424,7 @@ function AddChannelForm({
<button
onClick={handleSubmit}
disabled={!channelId.trim() || isLoading}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
<CheckIcon />
{t('admin.channelSubscriptions.form.submit')}
@@ -499,7 +499,7 @@ function EditChannelForm({
<button
onClick={handleSubmit}
disabled={isLoading}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
<CheckIcon />
{t('admin.channelSubscriptions.form.save')}
@@ -641,7 +641,7 @@ export default function AdminChannelSubscriptions() {
<button
onClick={() => setShowAddForm(true)}
aria-label={t('admin.channelSubscriptions.addChannel')}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
<span className="hidden sm:inline">{t('admin.channelSubscriptions.addChannel')}</span>

View File

@@ -442,7 +442,7 @@ function TemplateEditor({
<button
onClick={() => saveMutation.mutate()}
disabled={!isDirty || saveMutation.isPending}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent-500 px-3 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-40 sm:px-4 sm:py-2"
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent-500 px-3 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-40 sm:px-4 sm:py-2"
>
<SaveIcon className="h-4 w-4" />
{saveMutation.isPending ? t('common.loading') : t('common.save')}

View File

@@ -1027,7 +1027,7 @@ export default function AdminInfoPageEditor() {
<button
onClick={handleSave}
disabled={saveMutation.isPending || !slug.trim()}
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{saveMutation.isPending ? t('admin.infoPages.saving') : t('admin.infoPages.save')}
</button>
@@ -1108,7 +1108,7 @@ export default function AdminInfoPageEditor() {
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
displayMode === mode
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>
@@ -1136,7 +1136,7 @@ export default function AdminInfoPageEditor() {
pageType === pt
? pt === 'faq'
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>
@@ -1194,7 +1194,7 @@ export default function AdminInfoPageEditor() {
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
activeLocale === loc
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>
@@ -1415,7 +1415,7 @@ export default function AdminInfoPageEditor() {
<button
onClick={handleSave}
disabled={saveMutation.isPending || !slug.trim()}
className="min-h-[44px] w-full rounded-lg bg-accent-500 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="min-h-[44px] w-full rounded-lg bg-accent-500 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{saveMutation.isPending ? t('admin.infoPages.saving') : t('admin.infoPages.save')}
</button>

View File

@@ -238,7 +238,7 @@ export default function AdminInfoPages() {
haptic.buttonPress();
navigate('/admin/info-pages/create?type=faq');
}}
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-warning-500/80 px-4 py-2.5 text-white transition-colors hover:bg-warning-500"
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-warning-500 px-4 py-2.5 text-on-warning transition-colors hover:bg-warning-400"
aria-label={t('admin.infoPages.createFaq')}
>
<PlusIcon />
@@ -249,7 +249,7 @@ export default function AdminInfoPages() {
haptic.buttonPress();
navigate('/admin/info-pages/create');
}}
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600"
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600"
aria-label={t('admin.infoPages.create')}
>
<PlusIcon />
@@ -268,7 +268,7 @@ export default function AdminInfoPages() {
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
activeFilter === tab
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>

View File

@@ -588,7 +588,7 @@ export default function AdminLandingEditor() {
<button
onClick={handleSubmit}
disabled={isPending || !slug || !Object.values(title).some((v) => v.trim())}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{isPending && (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />

View File

@@ -423,7 +423,7 @@ export default function AdminLandings() {
)}
<button
onClick={() => navigate('/admin/landings/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.landings.create')}

View File

@@ -49,7 +49,7 @@ function DisplayModeSelector({
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
value === mode
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
disabled && 'cursor-not-allowed opacity-50',
)}
@@ -87,7 +87,7 @@ function LanguageTabs({
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium uppercase transition-colors',
active === lang
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>
@@ -221,7 +221,7 @@ function DocumentEditor({
<button
onClick={() => saveMutation.mutate()}
disabled={saveMutation.isPending}
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{saveMutation.isPending ? t('admin.legalPages.saving') : t('admin.legalPages.save')}
</button>
@@ -321,7 +321,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi
<button
onClick={() => saveMutation.mutate()}
disabled={saveMutation.isPending}
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{saveMutation.isPending ? t('admin.legalPages.saving') : t('admin.legalPages.save')}
</button>
@@ -604,7 +604,7 @@ function FaqEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => void
<button
onClick={() => createMutation.mutate()}
disabled={createMutation.isPending}
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<PlusIcon />
<span className="hidden sm:inline">{t('admin.legalPages.addQuestion')}</span>
@@ -669,7 +669,7 @@ export default function AdminLegalPages() {
className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
activeTab === tab
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
)}
>

View File

@@ -273,7 +273,7 @@ export default function AdminNews() {
haptic.buttonPress();
navigate('/admin/news/create');
}}
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600"
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600"
aria-label={t('news.admin.create')}
>
<PlusIcon />

View File

@@ -558,7 +558,7 @@ export default function AdminNewsCreate() {
<button
onClick={handleSave}
disabled={saveMutation.isPending || !title.trim() || !slug.trim() || !selectedCategory}
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="min-h-[44px] rounded-lg bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{saveMutation.isPending ? t('news.admin.saving') : t('news.admin.save')}
</button>
@@ -912,7 +912,7 @@ export default function AdminNewsCreate() {
<button
onClick={handleSave}
disabled={saveMutation.isPending || !title.trim() || !slug.trim() || !selectedCategory}
className="min-h-[44px] w-full rounded-lg bg-accent-500 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="min-h-[44px] w-full rounded-lg bg-accent-500 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{saveMutation.isPending ? t('news.admin.saving') : t('news.admin.save')}
</button>

View File

@@ -68,7 +68,7 @@ export default function AdminPartnerCampaignAssign() {
</div>
<button
onClick={() => navigate(`/admin/campaigns/create?partnerId=${userId}`)}
className="mt-2 w-full rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600"
className="mt-2 w-full rounded-lg bg-accent-500 px-4 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('admin.partnerDetail.campaigns.createNew')}
</button>

View File

@@ -90,7 +90,7 @@ export default function AdminPartnerCommission() {
Number(commissionValue) < 1 ||
Number(commissionValue) > 100
}
className="flex-1 rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex-1 rounded-lg bg-accent-500 px-4 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{updateMutation.isPending ? t('common.saving') : t('common.save')}
</button>

View File

@@ -301,7 +301,9 @@ export default function AdminPaymentMethodEdit() {
<span className="text-sm">{opt.name}</span>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${
enabled ? 'bg-accent-500 text-white' : 'border border-dark-600 bg-dark-700'
enabled
? 'bg-accent-500 text-on-accent'
: 'border border-dark-600 bg-dark-700'
}`}
>
{enabled && <CheckIcon />}
@@ -493,7 +495,7 @@ export default function AdminPaymentMethodEdit() {
<span>{group.name}</span>
<div
className={`flex h-4 w-4 items-center justify-center rounded ${
selected ? 'bg-accent-500 text-white' : 'border border-dark-600'
selected ? 'bg-accent-500 text-on-accent' : 'border border-dark-600'
}`}
>
{selected && <CheckIcon />}

View File

@@ -218,7 +218,7 @@ export default function AdminPaymentMethods() {
</button>
)}
<div>
<h1 className="text-2xl font-bold text-dark-50">{t('admin.paymentMethods.title')}</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.paymentMethods.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.paymentMethods.description')}</p>
</div>
</div>

View File

@@ -184,7 +184,7 @@ export default function AdminPayments() {
</button>
)}
<div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.payments.title')}</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.payments.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.payments.description')}</p>
</div>
</div>
@@ -239,7 +239,7 @@ export default function AdminPayments() {
onClick={() => setStatusFilter(option.value)}
className={`rounded-lg px-3 py-1.5 text-sm transition-all ${
statusFilter === option.value
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
@@ -256,7 +256,7 @@ export default function AdminPayments() {
onClick={() => handlePeriodChange(option.value)}
className={`rounded-lg px-3 py-1.5 text-sm transition-all ${
periodFilter === option.value
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
@@ -267,7 +267,7 @@ export default function AdminPayments() {
onClick={() => handlePeriodChange('custom')}
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-all ${
periodFilter === 'custom'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>

View File

@@ -269,7 +269,7 @@ export default function AdminPinnedMessages() {
</button>
<button
onClick={() => navigate('/admin/pinned-messages/create')}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
<span className="hidden sm:inline">{t('admin.pinnedMessages.create')}</span>

View File

@@ -228,7 +228,7 @@ export default function AdminPolicies() {
<PermissionGate permission="roles:create">
<button
onClick={() => navigate('/admin/policies/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.policies.createPolicy')}

View File

@@ -757,7 +757,7 @@ export default function AdminPolicyEdit() {
<button
type="submit"
disabled={isSaving}
className="rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{isSaving ? t('admin.policies.form.saving') : t('admin.policies.form.save')}
</button>

View File

@@ -65,7 +65,7 @@ export default function AdminPromoGroups() {
</div>
<button
onClick={() => navigate('/admin/promo-groups/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.promoGroups.addGroup')}

View File

@@ -199,7 +199,7 @@ export default function AdminPromoOfferSend() {
<div className="flex justify-center gap-3">
<button
onClick={() => navigate('/admin/promo-offers')}
className="rounded-lg bg-accent-500 px-6 py-2 text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 px-6 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
{t('admin.promoOffers.backToList')}
</button>
@@ -441,7 +441,7 @@ export default function AdminPromoOfferSend() {
{selectedTemplate.message_text}
</div>
<div className="mt-4">
<span className="inline-block rounded-lg bg-accent-500 px-4 py-2 text-sm text-white">
<span className="inline-block rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent">
{selectedTemplate.button_text}
</span>
</div>

View File

@@ -93,7 +93,7 @@ export default function AdminPromoOffers() {
</div>
<button
onClick={() => navigate('/admin/promo-offers/send')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<SendIcon />
{t('admin.promoOffers.sendButton')}

View File

@@ -124,7 +124,7 @@ export default function AdminPromocodeStats() {
</div>
<button
onClick={() => navigate(`/admin/promocodes/${id}/edit`)}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<EditIcon />
{t('admin.promocodes.modal.edit')}

View File

@@ -102,13 +102,13 @@ export default function AdminPromocodes() {
</button>
)}
<div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promocodes.title')}</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.promocodes.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.promocodes.subtitle')}</p>
</div>
</div>
<button
onClick={() => navigate('/admin/promocodes/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.promocodes.addPromocode')}

View File

@@ -432,7 +432,7 @@ export default function AdminRoleAssign() {
<button
type="submit"
disabled={assignMutation.isPending || !selectedUser || !selectedRoleId}
className="flex items-center gap-2 rounded-lg bg-accent-500 px-5 py-2 text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
className="flex items-center gap-2 rounded-lg bg-accent-500 px-5 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{assignMutation.isPending
? t('admin.roleAssign.assigning')

View File

@@ -514,7 +514,7 @@ export default function AdminRoleEdit() {
<button
type="submit"
disabled={isSaving}
className="rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{isSaving ? t('admin.roles.form.saving') : t('admin.roles.form.save')}
</button>

View File

@@ -81,7 +81,7 @@ export default function AdminRoles() {
<PermissionGate permission="roles:create">
<button
onClick={() => navigate('/admin/roles/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.roles.createRole')}

View File

@@ -76,7 +76,7 @@ export default function AdminServers() {
<button
onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<SyncIcon />
{syncMutation.isPending ? t('admin.servers.syncing') : t('admin.servers.sync')}

View File

@@ -248,7 +248,7 @@ export default function AdminSettings() {
<BackIcon />
</button>
)}
<h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.settings.title')}</h1>
</div>
</div>
<SettingsTreeSidebar

View File

@@ -661,7 +661,7 @@ export default function AdminTariffCreate() {
!selectedExternalSquad
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>
@@ -691,7 +691,7 @@ export default function AdminTariffCreate() {
isSelected
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>
@@ -740,7 +740,7 @@ export default function AdminTariffCreate() {
isSelected
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>
@@ -1055,7 +1055,7 @@ export default function AdminTariffCreate() {
isSelected
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-accent-500 text-on-accent'
: 'bg-dark-600'
}`}
>

View File

@@ -307,7 +307,7 @@ export default function AdminTariffs() {
</button>
)}
<div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.tariffs.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
</div>
</div>
@@ -328,7 +328,7 @@ export default function AdminTariffs() {
)}
<button
onClick={() => navigate('/admin/tariffs/create')}
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon />
{t('admin.tariffs.create')}

View File

@@ -272,9 +272,7 @@ export default function AdminTickets() {
<BackIcon />
</button>
)}
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
{t('admin.tickets.title')}
</h1>
<h1 className="text-xl font-bold text-dark-100">{t('admin.tickets.title')}</h1>
</div>
<button
onClick={() => navigate('/admin/tickets/settings')}

View File

@@ -740,7 +740,7 @@ export default function AdminUserDetail() {
<p className="text-dark-400">{t('admin.users.notFound')}</p>
<button
onClick={() => navigate('/admin/users')}
className="rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.back')}
</button>

View File

@@ -364,7 +364,7 @@ export default function AdminWithdrawalDetail() {
<button
onClick={() => completeMutation.mutate()}
disabled={completeMutation.isPending}
className="w-full rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="w-full rounded-lg bg-accent-500 px-4 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{completeMutation.isPending
? t('admin.withdrawals.detail.completing')

View File

@@ -81,7 +81,7 @@ export default function AdminWithdrawals() {
onClick={() => setStatusFilter(filter)}
className={`whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
statusFilter === filter
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800/40 text-dark-400 hover:bg-dark-700/50 hover:text-dark-200'
}`}
>

View File

@@ -58,7 +58,7 @@ export default function AutoLogin() {
<button
type="button"
onClick={() => navigate('/login', { replace: true })}
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('auth.login', 'Login')}
</button>

View File

@@ -340,7 +340,7 @@ export default function Balance() {
{translatedDesc || method.description}
</div>
)}
<div className="mt-3 text-xs text-dark-600">
<div className="mt-3 text-xs text-dark-400">
{formatAmount(method.min_amount_kopeks / 100, 0)} {t('common.rangeTo', 'to')}{' '}
{formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div>

View File

@@ -318,7 +318,7 @@ export default function Dashboard() {
) : (
<Link
to="/subscription/purchase"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
<span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
@@ -381,7 +381,7 @@ export default function Dashboard() {
)}
<Link
to="/subscription/purchase"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
<span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}

View File

@@ -189,7 +189,7 @@ export default function GiftClaim() {
'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3 text-sm font-bold transition-all active:scale-[0.98]',
copied
? 'bg-success-500/20 text-success-400'
: 'bg-accent-500 text-white hover:bg-accent-400',
: 'bg-accent-500 text-on-accent hover:bg-accent-400',
)}
>
{copied ? <CheckIcon className="h-4 w-4" /> : <CopyIcon className="h-4 w-4" />}
@@ -264,7 +264,7 @@ export default function GiftClaim() {
{gift.bot_claim_link && (
<a
href={gift.bot_claim_link}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3.5 text-sm font-bold text-white shadow-lg shadow-accent-500/25 transition-all hover:bg-accent-400 active:scale-[0.98]"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3.5 text-sm font-bold text-on-accent shadow-lg shadow-accent-500/25 transition-all hover:bg-accent-400 active:scale-[0.98]"
>
{t('landing.giftClaim.activateTelegram', 'Activate in Telegram')}
</a>
@@ -303,7 +303,7 @@ export default function GiftClaim() {
className={cn(
'flex w-full items-center justify-center gap-2 rounded-xl px-6 py-3.5 text-sm font-bold transition-all',
isValidEmail(email) && !claimMutation.isPending
? 'bg-accent-500 text-white hover:bg-accent-400 active:scale-[0.98]'
? 'bg-accent-500 text-on-accent hover:bg-accent-400 active:scale-[0.98]'
: 'cursor-not-allowed bg-dark-800 text-dark-500',
)}
>

View File

@@ -156,7 +156,7 @@ function CodeOnlySuccessState({
'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3.5 text-sm font-bold transition-all duration-200 active:scale-[0.98]',
copied
? 'bg-success-500/20 text-success-400'
: 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400',
: 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400',
)}
>
{copied ? (
@@ -236,7 +236,7 @@ function DeliveredState({
<button
type="button"
onClick={() => navigate('/')}
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.backToDashboard', 'Back to dashboard')}
</button>
@@ -303,7 +303,7 @@ function PendingActivationState({
<button
type="button"
onClick={() => navigate('/')}
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.backToDashboard', 'Back to dashboard')}
</button>
@@ -335,7 +335,7 @@ function FailedState() {
<button
type="button"
onClick={() => navigate('/gift')}
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.tryAgain', 'Try again')}
</button>
@@ -372,7 +372,7 @@ function PollErrorState() {
<button
type="button"
onClick={() => navigate('/')}
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.backToDashboard', 'Back to dashboard')}
</button>
@@ -406,7 +406,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
<button
type="button"
onClick={onRetry}
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.retry', 'Retry')}
</button>
@@ -436,7 +436,7 @@ function NoTokenState() {
<button
type="button"
onClick={() => navigate('/gift')}
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('gift.backToGift', 'Go back')}
</button>

View File

@@ -231,7 +231,7 @@ function PeriodCard({
<span
className={cn(
'rounded-md px-2 py-0.5 text-xs font-bold',
isSelected ? 'bg-white/20 text-white' : 'bg-accent-500/20 text-accent-400',
isSelected ? 'bg-white/20 text-on-accent' : 'bg-accent-500/20 text-accent-400',
)}
>
-{period.discount_percent}%
@@ -366,7 +366,7 @@ function PaymentMethodCard({
className={cn(
'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200',
selectedSubOption === opt.id
? 'bg-accent-500 text-white shadow-sm shadow-accent-500/25'
? 'bg-accent-500 text-on-accent shadow-sm shadow-accent-500/25'
: 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50',
)}
>
@@ -716,7 +716,7 @@ function BuyTabContent({
className={cn(
'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200',
canSubmit && !purchaseMutation.isPending
? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
: 'cursor-not-allowed bg-dark-800 text-dark-500',
)}
>
@@ -833,7 +833,7 @@ function ActivateTabContent({ initialCode }: { initialCode?: string | null }) {
className={cn(
'w-full max-w-sm rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200',
code.trim() && !activateMutation.isPending
? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 active:scale-[0.98]'
? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 active:scale-[0.98]'
: 'cursor-not-allowed bg-dark-800 text-dark-500',
)}
>
@@ -962,7 +962,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
<button
type="button"
onClick={handleShare}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-4 py-3 text-sm font-bold uppercase tracking-wider text-white transition-colors hover:bg-accent-400 active:scale-[0.98]"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-4 py-3 text-sm font-bold uppercase tracking-wider text-on-accent transition-colors hover:bg-accent-400 active:scale-[0.98]"
>
<ExportIcon className="h-4 w-4" />
{t('gift.shareGift')}
@@ -1243,7 +1243,7 @@ export default function GiftSubscription() {
className={cn(
'flex-1 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200',
activeTab === tab.id
? 'bg-accent-500 text-white shadow-sm'
? 'bg-accent-500 text-on-accent shadow-sm'
: 'text-dark-400 hover:text-dark-200',
)}
>

View File

@@ -838,7 +838,7 @@ export default function Info() {
onClick={() => setActiveTab(tab.id)}
className={`flex min-h-[44px] shrink-0 items-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>

View File

@@ -583,7 +583,7 @@ export default function Login() {
type="button"
className={`flex-1 rounded-md py-2 text-sm font-medium transition-all ${
authMode === 'login'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'text-dark-400 hover:text-dark-200'
}`}
onClick={() => setAuthMode('login')}
@@ -594,7 +594,7 @@ export default function Login() {
type="button"
className={`flex-1 rounded-md py-2 text-sm font-medium transition-all ${
authMode === 'register'
? 'bg-accent-500 text-white'
? 'bg-accent-500 text-on-accent'
: 'text-dark-400 hover:text-dark-200'
}`}
onClick={() => setAuthMode('register')}

View File

@@ -268,7 +268,7 @@ function SuccessState({
href={botLink}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('landing.openBot')}
</a>
@@ -492,7 +492,7 @@ function GiftPendingActivationState({
href={botLink}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('landing.openBot')}
</a>
@@ -545,7 +545,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
<button
type="button"
onClick={onRetry}
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('common.retry', 'Retry')}
</button>
@@ -641,7 +641,7 @@ function GiftLinkShareState({
'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3.5 text-sm font-bold transition-all duration-200 active:scale-[0.98]',
copied
? 'bg-success-500/20 text-success-400'
: 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400',
: 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400',
)}
>
{copied ? (

View File

@@ -122,7 +122,7 @@ function PeriodTabs({
className={cn(
'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200',
selectedDays === period.days
? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25'
? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25'
: 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
)}
>
@@ -409,7 +409,7 @@ function PaymentMethodCard({
className={cn(
'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200',
selectedSubOption === opt.id
? 'bg-accent-500 text-white shadow-sm shadow-accent-500/25'
? 'bg-accent-500 text-on-accent shadow-sm shadow-accent-500/25'
: 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
)}
>
@@ -583,7 +583,7 @@ function SummaryCard({
className={cn(
'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200',
canSubmit && !isSubmitting
? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
: 'cursor-not-allowed bg-dark-800 text-dark-500',
)}
>
@@ -629,7 +629,7 @@ function SummaryCard({
className={cn(
'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200',
canSubmit && !isSubmitting
? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]'
: 'cursor-not-allowed bg-dark-800 text-dark-500',
)}
>
@@ -734,7 +734,7 @@ function DiscountBanner({
<div className="flex flex-col items-center gap-4 px-5 py-5 sm:flex-row sm:justify-between">
{/* Left: badge + text */}
<div className="flex items-center gap-3">
<span className="shrink-0 rounded-full bg-accent-500 px-3 py-1 text-sm font-bold text-white shadow-lg shadow-accent-500/25">
<span className="shrink-0 rounded-full bg-accent-500 px-3 py-1 text-sm font-bold text-on-accent shadow-lg shadow-accent-500/25">
-{discount.percent}%
</span>
{discount.badge_text && (

View File

@@ -226,7 +226,7 @@ export default function RenewSubscription() {
<button
onClick={() => handleRenew(selectedPeriod)}
disabled={renewMutation.isPending}
className="w-full rounded-2xl bg-accent-500 py-3.5 text-base font-semibold text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
className="w-full rounded-2xl bg-accent-500 py-3.5 text-base font-semibold text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{renewMutation.isPending
? t('common.processing', 'Обработка...')

View File

@@ -515,7 +515,7 @@ export default function Subscription() {
</p>
<button
onClick={() => navigate('/subscriptions')}
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-white"
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-on-accent"
>
{t('subscription.backToList', 'Мои подписки')}
</button>

View File

@@ -121,7 +121,7 @@ export default function SubscriptionPurchase() {
</p>
<button
onClick={() => refetchOptions()}
className="rounded-xl bg-accent-500 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-6 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.retry')}
</button>
@@ -322,7 +322,7 @@ export default function SubscriptionPurchase() {
</p>
<button
onClick={() => refetchOptions()}
className="rounded-xl bg-accent-500 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-6 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('common.retry')}
</button>

View File

@@ -35,7 +35,7 @@ function EmptyState({ onBuy }: { onBuy: () => void }) {
</p>
<button
onClick={onBuy}
className="rounded-xl bg-accent-500 px-8 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-xl bg-accent-500 px-8 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('subscriptions.buy', 'Купить подписку')}
</button>
@@ -135,7 +135,7 @@ export default function Subscriptions() {
{!isLoading && subscriptions.length > 0 && !hasActivePaid && (
<button
onClick={() => navigate('/subscription/purchase')}
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
@@ -171,7 +171,7 @@ export default function Subscriptions() {
(Telegram-баг #605056/#605063). */}
<button
onClick={() => navigate('/subscription/purchase')}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-600"
>
<PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}

View File

@@ -507,7 +507,7 @@ export default function TopUpAmount() {
? 'cursor-not-allowed bg-dark-700 text-dark-500'
: isStarsMethod
? 'bg-gradient-to-r from-yellow-500 to-orange-500 text-white shadow-lg shadow-yellow-500/25 hover:from-yellow-400 hover:to-orange-400 active:from-yellow-600 active:to-orange-600'
: 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600'
: 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600'
}`}
>
{isPending ? (

View File

@@ -82,7 +82,7 @@ export default function TopUpMethodSelect() {
{translatedDesc || method.description}
</div>
)}
<div className="mt-3 text-xs text-dark-600">
<div className="mt-3 text-xs text-dark-400">
{formatAmount(method.min_amount_kopeks / 100, 0)} {' '}
{formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div>

View File

@@ -85,7 +85,7 @@ function SuccessState({ amountKopeks }: { amountKopeks: number | null }) {
<button
type="button"
onClick={handleGoToBalance}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('balance.topUpResult.goToBalance')}
</button>
@@ -162,7 +162,7 @@ function TimeoutState({ onRetry, onGoBack }: { onRetry: () => void; onGoBack: ()
<button
type="button"
onClick={onRetry}
className="w-full rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
className="w-full rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-on-accent transition-colors hover:bg-accent-400"
>
{t('common.retry')}
</button>

View File

@@ -632,7 +632,7 @@ export default function Wheel() {
</button>
<button
onClick={handleDirectStarsPay}
className="rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
className="rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{t('wheel.payStars', { count: config.spin_cost_stars ?? 0 })}
</button>

View File

@@ -342,6 +342,20 @@
--color-dark-850: var(--color-champagne-100) !important;
--color-dark-900: var(--color-champagne-100) !important;
--color-dark-950: var(--color-champagne-200) !important;
/* Статусные палитры генерируются под тёмную тему: текстовые оттенки
300/400 (зелёные суммы, жёлтая «Админка», бейджи) на светлом фоне
дают контраст 1.31.6. В светлой теме подменяем их тёмными шейдами —
зеркально ремапу серых выше. Фоновые употребления (bg-*-500/15 и т.п.)
используют 500+ и не затрагиваются. */
--color-accent-300: var(--color-accent-700) !important;
--color-accent-400: var(--color-accent-700) !important;
--color-success-300: var(--color-success-700) !important;
--color-success-400: var(--color-success-700) !important;
--color-warning-300: var(--color-warning-700) !important;
--color-warning-400: var(--color-warning-700) !important;
--color-error-300: var(--color-error-700) !important;
--color-error-400: var(--color-error-700) !important;
}
/* Custom scrollbar - Dark */
@@ -578,7 +592,7 @@ img.twemoji {
}
.btn-primary {
@apply btn bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-xl hover:shadow-accent-500/30 focus-visible:ring-accent-500 active:bg-accent-600;
@apply btn bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-xl hover:shadow-accent-500/30 focus-visible:ring-accent-500 active:bg-accent-600;
}
.btn-secondary {

View File

@@ -1,9 +1,12 @@
import i18n from '../i18n';
/**
* Format traffic amount with appropriate unit (MB/GB/TB).
* Matches the prototype's formatBytes logic.
* Units come from i18n so RU shows «ГБ» consistently with the rest of the UI
* (templates previously mixed latin "GB" with localized «ГБ» on one screen).
*/
export function formatTraffic(gb: number): string {
if (gb >= 1000) return `${(gb / 1000).toFixed(1)} TB`;
if (gb >= 1) return `${gb.toFixed(1)} GB`;
return `${(gb * 1024).toFixed(0)} MB`;
if (gb >= 1000) return `${(gb / 1000).toFixed(1)} ${i18n.t('common.units.tb', 'TB')}`;
if (gb >= 1) return `${gb.toFixed(1)} ${i18n.t('common.units.gb', 'GB')}`;
return `${(gb * 1024).toFixed(0)} ${i18n.t('common.units.mb', 'MB')}`;
}

View File

@@ -45,6 +45,12 @@ export default {
900: withOpacity('--color-champagne-900'),
950: withOpacity('--color-champagne-950'),
},
// Readable text on top of status-colored fills (computed from the
// operator palette in useThemeColors — black or white, whichever reads)
'on-accent': withOpacity('--color-on-accent'),
'on-success': withOpacity('--color-on-success'),
'on-warning': withOpacity('--color-on-warning'),
'on-error': withOpacity('--color-on-error'),
// Accent - dynamic color scheme
accent: {
50: withOpacity('--color-accent-50'),