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> </p>
<button <button
onClick={() => window.location.reload()} 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 Reload page
</button> </button>
@@ -110,7 +110,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
</p> </p>
<button <button
onClick={() => window.location.reload()} 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'} {isChunk ? 'Reload' : 'Try again'}
</button> </button>

View File

@@ -281,7 +281,7 @@ export default function SuccessNotificationModal() {
{isSubscription && ( {isSubscription && (
<button <button
onClick={handleGoToSubscription} 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" /> <RocketIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span> <span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
@@ -301,7 +301,7 @@ export default function SuccessNotificationModal() {
{isDevicesPurchased && ( {isDevicesPurchased && (
<button <button
onClick={handleGoToSubscription} 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" /> <DevicesIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span> <span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>

View File

@@ -114,7 +114,7 @@ export function AnalyticsTab() {
<button <button
onClick={handleSaveYandex} onClick={handleSaveYandex}
disabled={updateMutation.isPending} 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 /> <CheckIcon />
</button> </button>
@@ -253,7 +253,7 @@ export function AnalyticsTab() {
<button <button
onClick={handleSaveGoogleId} onClick={handleSaveGoogleId}
disabled={updateMutation.isPending} 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 /> <CheckIcon />
</button> </button>
@@ -307,7 +307,7 @@ export function AnalyticsTab() {
<button <button
onClick={handleSaveGoogleLabel} onClick={handleSaveGoogleLabel}
disabled={updateMutation.isPending} 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 /> <CheckIcon />
</button> </button>

View File

@@ -68,7 +68,7 @@ export function BackgroundEditor() {
'w-full rounded-xl py-3 text-sm font-medium transition-colors', 'w-full rounded-xl py-3 text-sm font-medium transition-colors',
saveStatus === 'saved' saveStatus === 'saved'
? 'bg-success-500/20 text-success-400' ? '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' {saveStatus === 'saving'

View File

@@ -168,7 +168,7 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) {
<button <button
onClick={() => updateBrandingMutation.mutate(newName)} onClick={() => updateBrandingMutation.mutate(newName)}
disabled={updateBrandingMutation.isPending} 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 /> <CheckIcon />
</button> </button>

View File

@@ -228,7 +228,7 @@ export function ButtonsTab() {
? 'bg-success-500 text-white' ? 'bg-success-500 text-white'
: cfg.style === 'danger' : cfg.style === 'danger'
? 'bg-error-500 text-white' ? 'bg-error-500 text-white'
: 'bg-accent-500 text-white' : 'bg-accent-500 text-on-accent'
}`} }`}
> >
{t(`admin.buttons.styles.${cfg.style}`)} {t(`admin.buttons.styles.${cfg.style}`)}
@@ -321,7 +321,7 @@ export function ButtonsTab() {
<button <button
onClick={handleSave} onClick={handleSave}
disabled={updateMutation.isPending} 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')} {updateMutation.isPending ? t('common.saving') : t('common.save')}
</button> </button>

View File

@@ -346,7 +346,7 @@ export function ColoredItemCombobox({
type="button" type="button"
onClick={handleCreate} onClick={handleCreate}
disabled={isCreating} 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 ? ( {isCreating ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> <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)} onClick={() => onChange(n)}
className={`flex h-7 w-7 items-center justify-center rounded-lg text-xs font-semibold transition-all ${ className={`flex h-7 w-7 items-center justify-center rounded-lg text-xs font-semibold transition-all ${
value === n 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' : 'bg-dark-700/50 text-dark-400 hover:bg-dark-600 hover:text-dark-300'
}`} }`}
> >
@@ -788,7 +788,7 @@ export function MenuEditorTab() {
<button <button
onClick={handleSave} onClick={handleSave}
disabled={updateMutation.isPending} 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')} {updateMutation.isPending ? t('common.saving') : t('common.save')}
</button> </button>

View File

@@ -122,7 +122,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
</button> </button>
<button <button
onClick={handleSave} 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 /> <CheckIcon />
{t('admin.settings.saveButton')} {t('admin.settings.saveButton')}
@@ -159,7 +159,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
/> />
<button <button
onClick={handleSave} 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')} title={t('admin.settings.saveHint')}
> >
<CheckIcon /> <CheckIcon />

View File

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

View File

@@ -428,7 +428,7 @@ export function ThemeTab() {
<button <button
onClick={() => updateColorsMutation.mutate(draftColors)} onClick={() => updateColorsMutation.mutate(draftColors)}
disabled={updateColorsMutation.isPending} 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 {updateColorsMutation.isPending
? t('common.saving', t('common.save')) ? t('common.saving', t('common.save'))

View File

@@ -621,7 +621,7 @@ export function ActionModal({
<button <button
onClick={onClose} 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')} {t('common.close')}
</button> </button>
@@ -647,7 +647,7 @@ export function ActionModal({
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isConfirmDisabled} 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')} {t('admin.bulkActions.confirm')}
</button> </button>

View File

@@ -208,7 +208,7 @@ export function FloatingActionBar({
<div className="relative ml-auto"> <div className="relative ml-auto">
<button <button
onClick={() => setOpen(!open)} 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')} {t('common.actions')}
<ChevronDownIcon /> <ChevronDownIcon />

View File

@@ -52,7 +52,7 @@ export function CountryFilter({
> >
<GlobeIcon className="h-4 w-4" /> <GlobeIcon className="h-4 w-4" />
{activeCount > 0 && ( {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} {activeCount}
</span> </span>
)} )}

View File

@@ -56,7 +56,7 @@ export function NodeFilter({
<ServerIcon className="h-4 w-4" /> <ServerIcon className="h-4 w-4" />
{t('admin.trafficUsage.nodes')} {t('admin.trafficUsage.nodes')}
{activeCount > 0 && ( {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} {activeCount}
</span> </span>
)} )}

View File

@@ -79,7 +79,7 @@ export function PeriodSelector({
onClick={() => onChange(p)} onClick={() => onChange(p)}
className={`rounded-lg px-2.5 py-1 text-xs font-medium transition-colors ${ className={`rounded-lg px-2.5 py-1 text-xs font-medium transition-colors ${
value === p 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' : '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" /> <StatusIcon className="h-4 w-4" />
{t('admin.trafficUsage.status')} {t('admin.trafficUsage.status')}
{activeCount > 0 && ( {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} {activeCount}
</span> </span>
)} )}

View File

@@ -54,7 +54,7 @@ export function TariffFilter({
<FilterIcon className="h-4 w-4" /> <FilterIcon className="h-4 w-4" />
{t('admin.trafficUsage.tariff')} {t('admin.trafficUsage.tariff')}
{activeCount > 0 && ( {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} {activeCount}
</span> </span>
)} )}

View File

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

View File

@@ -464,7 +464,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
<button <button
onClick={() => selectedTrafficGb && onAddTraffic(Number(selectedTrafficGb))} onClick={() => selectedTrafficGb && onAddTraffic(Number(selectedTrafficGb))}
disabled={actionLoading || !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')} {t('admin.users.detail.subscription.addButton')}
</button> </button>

View File

@@ -348,7 +348,7 @@ function ChatView({
onClick={onReply} onClick={onReply}
disabled={!replyText.trim() || replySending} disabled={!replyText.trim() || replySending}
aria-label={t('admin.tickets.sendReply', 'Send reply')} 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 ? ( {replySending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <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 */} {/* Activate button */}
<Link <Link
to={`/gift?tab=activate&code=${gift.token}`} 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')} {t('gift.pending.activate')}
</Link> </Link>

View File

@@ -80,7 +80,7 @@ export function MobileBottomNav({
onClick={handleNavClick} onClick={handleNavClick}
className={cn( 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', '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) && ( {isActive(item.path) && (

View File

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

View File

@@ -107,7 +107,7 @@ export function TariffPickerGrid({
</p> </p>
<button <button
onClick={() => navigate('/subscriptions')} 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', 'Мои подписки')} {t('subscription.backToList', 'Мои подписки')}
</button> </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) // Apply theme colors as CSS variables (RGB format for Tailwind opacity support)
export function applyThemeColors(colors: ThemeColors): void { export function applyThemeColors(colors: ThemeColors): void {
const root = document.documentElement; const root = document.documentElement;
@@ -70,6 +125,17 @@ export function applyThemeColors(colors: ThemeColors): void {
const darkTextRgb = hexToRgb(colors.darkText); const darkTextRgb = hexToRgb(colors.darkText);
const darkTextSecRgb = hexToRgb(colors.darkTextSecondary); 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: // Apply dark palette with actual user colors:
// Text colors (light shades): 50-100 = primary text, 200-300 = mixed, 400 = secondary text // Text colors (light shades): 50-100 = primary text, 200-300 = mixed, 400 = secondary text
root.style.setProperty( 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-300', interpolateRgb(darkTextRgb, darkTextSecRgb, 0.66));
root.style.setProperty( root.style.setProperty(
'--color-dark-400', '--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 // 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-600', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.6));
root.style.setProperty('--color-dark-700', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.8)); 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-300', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.2));
root.style.setProperty('--color-champagne-400', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.4)); root.style.setProperty('--color-champagne-400', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.4));
// Transition colors (500-600): between bg and text // Transition colors (500-600): between bg and text.
root.style.setProperty('--color-champagne-500', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.6)); // 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( root.style.setProperty(
'--color-champagne-600', '--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 // 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]); 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) // Apply semantic colors (hex for direct use)
root.style.setProperty('--color-dark-bg', colors.darkBackground); root.style.setProperty('--color-dark-bg', colors.darkBackground);
root.style.setProperty('--color-dark-surface', colors.darkSurface); root.style.setProperty('--color-dark-surface', colors.darkSurface);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -702,7 +702,7 @@ export default function AdminBroadcastCreate() {
onClick={() => toggleButton(button.key)} onClick={() => toggleButton(button.key)}
className={`rounded-lg px-3 py-2 text-sm transition-colors ${ className={`rounded-lg px-3 py-2 text-sm transition-colors ${
selectedButtons.includes(button.key) 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' : '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')} onClick={() => setNewButtonActionType('callback')}
className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${ className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${
newButtonActionType === 'callback' 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' : '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')} onClick={() => setNewButtonActionType('url')}
className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${ className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${
newButtonActionType === 'url' 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' : '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> <p className="text-dark-400">{t('admin.broadcasts.notFound')}</p>
<button <button
onClick={() => navigate('/admin/broadcasts')} 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')} {t('common.back')}
</button> </button>

View File

@@ -117,7 +117,7 @@ export default function AdminBroadcasts() {
</button> </button>
<button <button
onClick={() => navigate('/admin/broadcasts/create')} 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 /> <PlusIcon />
<span className="hidden sm:inline">{t('admin.broadcasts.create')}</span> <span className="hidden sm:inline">{t('admin.broadcasts.create')}</span>

View File

@@ -79,7 +79,9 @@ function ServerSelector({
> >
<div <div
className={`flex h-5 w-5 items-center justify-center rounded ${ 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 />} {selected.includes(server.squad_uuid) && <CheckIcon />}

View File

@@ -79,7 +79,9 @@ function ServerSelector({
> >
<div <div
className={`flex h-5 w-5 items-center justify-center rounded ${ 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 />} {selected.includes(server.squad_uuid) && <CheckIcon />}

View File

@@ -136,7 +136,7 @@ export default function AdminCampaigns() {
</div> </div>
<button <button
onClick={() => navigate('/admin/campaigns/create')} 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 /> <PlusIcon />
{t('admin.campaigns.createButton')} {t('admin.campaigns.createButton')}

View File

@@ -424,7 +424,7 @@ function AddChannelForm({
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!channelId.trim() || isLoading} 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 /> <CheckIcon />
{t('admin.channelSubscriptions.form.submit')} {t('admin.channelSubscriptions.form.submit')}
@@ -499,7 +499,7 @@ function EditChannelForm({
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isLoading} 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 /> <CheckIcon />
{t('admin.channelSubscriptions.form.save')} {t('admin.channelSubscriptions.form.save')}
@@ -641,7 +641,7 @@ export default function AdminChannelSubscriptions() {
<button <button
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
aria-label={t('admin.channelSubscriptions.addChannel')} 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 /> <PlusIcon />
<span className="hidden sm:inline">{t('admin.channelSubscriptions.addChannel')}</span> <span className="hidden sm:inline">{t('admin.channelSubscriptions.addChannel')}</span>

View File

@@ -442,7 +442,7 @@ function TemplateEditor({
<button <button
onClick={() => saveMutation.mutate()} onClick={() => saveMutation.mutate()}
disabled={!isDirty || saveMutation.isPending} 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" /> <SaveIcon className="h-4 w-4" />
{saveMutation.isPending ? t('common.loading') : t('common.save')} {saveMutation.isPending ? t('common.loading') : t('common.save')}

View File

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

View File

@@ -238,7 +238,7 @@ export default function AdminInfoPages() {
haptic.buttonPress(); haptic.buttonPress();
navigate('/admin/info-pages/create?type=faq'); 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')} aria-label={t('admin.infoPages.createFaq')}
> >
<PlusIcon /> <PlusIcon />
@@ -249,7 +249,7 @@ export default function AdminInfoPages() {
haptic.buttonPress(); haptic.buttonPress();
navigate('/admin/info-pages/create'); 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')} aria-label={t('admin.infoPages.create')}
> >
<PlusIcon /> <PlusIcon />
@@ -268,7 +268,7 @@ export default function AdminInfoPages() {
className={cn( className={cn(
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors', 'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
activeFilter === tab 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', : '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 <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isPending || !slug || !Object.values(title).some((v) => v.trim())} 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 && ( {isPending && (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <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 <button
onClick={() => navigate('/admin/landings/create')} 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 /> <PlusIcon />
{t('admin.landings.create')} {t('admin.landings.create')}

View File

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

View File

@@ -558,7 +558,7 @@ export default function AdminNewsCreate() {
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saveMutation.isPending || !title.trim() || !slug.trim() || !selectedCategory} 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')} {saveMutation.isPending ? t('news.admin.saving') : t('news.admin.save')}
</button> </button>
@@ -912,7 +912,7 @@ export default function AdminNewsCreate() {
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saveMutation.isPending || !title.trim() || !slug.trim() || !selectedCategory} 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')} {saveMutation.isPending ? t('news.admin.saving') : t('news.admin.save')}
</button> </button>

View File

@@ -68,7 +68,7 @@ export default function AdminPartnerCampaignAssign() {
</div> </div>
<button <button
onClick={() => navigate(`/admin/campaigns/create?partnerId=${userId}`)} 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')} {t('admin.partnerDetail.campaigns.createNew')}
</button> </button>

View File

@@ -90,7 +90,7 @@ export default function AdminPartnerCommission() {
Number(commissionValue) < 1 || Number(commissionValue) < 1 ||
Number(commissionValue) > 100 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')} {updateMutation.isPending ? t('common.saving') : t('common.save')}
</button> </button>

View File

@@ -301,7 +301,9 @@ export default function AdminPaymentMethodEdit() {
<span className="text-sm">{opt.name}</span> <span className="text-sm">{opt.name}</span>
<div <div
className={`flex h-5 w-5 items-center justify-center rounded ${ 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 />} {enabled && <CheckIcon />}
@@ -493,7 +495,7 @@ export default function AdminPaymentMethodEdit() {
<span>{group.name}</span> <span>{group.name}</span>
<div <div
className={`flex h-4 w-4 items-center justify-center rounded ${ 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 />} {selected && <CheckIcon />}

View File

@@ -218,7 +218,7 @@ export default function AdminPaymentMethods() {
</button> </button>
)} )}
<div> <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> <p className="text-sm text-dark-400">{t('admin.paymentMethods.description')}</p>
</div> </div>
</div> </div>

View File

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

View File

@@ -269,7 +269,7 @@ export default function AdminPinnedMessages() {
</button> </button>
<button <button
onClick={() => navigate('/admin/pinned-messages/create')} 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 /> <PlusIcon />
<span className="hidden sm:inline">{t('admin.pinnedMessages.create')}</span> <span className="hidden sm:inline">{t('admin.pinnedMessages.create')}</span>

View File

@@ -228,7 +228,7 @@ export default function AdminPolicies() {
<PermissionGate permission="roles:create"> <PermissionGate permission="roles:create">
<button <button
onClick={() => navigate('/admin/policies/create')} 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 /> <PlusIcon />
{t('admin.policies.createPolicy')} {t('admin.policies.createPolicy')}

View File

@@ -757,7 +757,7 @@ export default function AdminPolicyEdit() {
<button <button
type="submit" type="submit"
disabled={isSaving} 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')} {isSaving ? t('admin.policies.form.saving') : t('admin.policies.form.save')}
</button> </button>

View File

@@ -65,7 +65,7 @@ export default function AdminPromoGroups() {
</div> </div>
<button <button
onClick={() => navigate('/admin/promo-groups/create')} 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 /> <PlusIcon />
{t('admin.promoGroups.addGroup')} {t('admin.promoGroups.addGroup')}

View File

@@ -199,7 +199,7 @@ export default function AdminPromoOfferSend() {
<div className="flex justify-center gap-3"> <div className="flex justify-center gap-3">
<button <button
onClick={() => navigate('/admin/promo-offers')} 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')} {t('admin.promoOffers.backToList')}
</button> </button>
@@ -441,7 +441,7 @@ export default function AdminPromoOfferSend() {
{selectedTemplate.message_text} {selectedTemplate.message_text}
</div> </div>
<div className="mt-4"> <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} {selectedTemplate.button_text}
</span> </span>
</div> </div>

View File

@@ -93,7 +93,7 @@ export default function AdminPromoOffers() {
</div> </div>
<button <button
onClick={() => navigate('/admin/promo-offers/send')} 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 /> <SendIcon />
{t('admin.promoOffers.sendButton')} {t('admin.promoOffers.sendButton')}

View File

@@ -124,7 +124,7 @@ export default function AdminPromocodeStats() {
</div> </div>
<button <button
onClick={() => navigate(`/admin/promocodes/${id}/edit`)} 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 /> <EditIcon />
{t('admin.promocodes.modal.edit')} {t('admin.promocodes.modal.edit')}

View File

@@ -102,13 +102,13 @@ export default function AdminPromocodes() {
</button> </button>
)} )}
<div> <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> <p className="text-sm text-dark-400">{t('admin.promocodes.subtitle')}</p>
</div> </div>
</div> </div>
<button <button
onClick={() => navigate('/admin/promocodes/create')} 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 /> <PlusIcon />
{t('admin.promocodes.addPromocode')} {t('admin.promocodes.addPromocode')}

View File

@@ -432,7 +432,7 @@ export default function AdminRoleAssign() {
<button <button
type="submit" type="submit"
disabled={assignMutation.isPending || !selectedUser || !selectedRoleId} 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 {assignMutation.isPending
? t('admin.roleAssign.assigning') ? t('admin.roleAssign.assigning')

View File

@@ -514,7 +514,7 @@ export default function AdminRoleEdit() {
<button <button
type="submit" type="submit"
disabled={isSaving} 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')} {isSaving ? t('admin.roles.form.saving') : t('admin.roles.form.save')}
</button> </button>

View File

@@ -81,7 +81,7 @@ export default function AdminRoles() {
<PermissionGate permission="roles:create"> <PermissionGate permission="roles:create">
<button <button
onClick={() => navigate('/admin/roles/create')} 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 /> <PlusIcon />
{t('admin.roles.createRole')} {t('admin.roles.createRole')}

View File

@@ -76,7 +76,7 @@ export default function AdminServers() {
<button <button
onClick={() => syncMutation.mutate()} onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending} 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 /> <SyncIcon />
{syncMutation.isPending ? t('admin.servers.syncing') : t('admin.servers.sync')} {syncMutation.isPending ? t('admin.servers.syncing') : t('admin.servers.sync')}

View File

@@ -248,7 +248,7 @@ export default function AdminSettings() {
<BackIcon /> <BackIcon />
</button> </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>
</div> </div>
<SettingsTreeSidebar <SettingsTreeSidebar

View File

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

View File

@@ -307,7 +307,7 @@ export default function AdminTariffs() {
</button> </button>
)} )}
<div> <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> <p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
</div> </div>
</div> </div>
@@ -328,7 +328,7 @@ export default function AdminTariffs() {
)} )}
<button <button
onClick={() => navigate('/admin/tariffs/create')} 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 /> <PlusIcon />
{t('admin.tariffs.create')} {t('admin.tariffs.create')}

View File

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

View File

@@ -364,7 +364,7 @@ export default function AdminWithdrawalDetail() {
<button <button
onClick={() => completeMutation.mutate()} onClick={() => completeMutation.mutate()}
disabled={completeMutation.isPending} 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 {completeMutation.isPending
? t('admin.withdrawals.detail.completing') ? t('admin.withdrawals.detail.completing')

View File

@@ -81,7 +81,7 @@ export default function AdminWithdrawals() {
onClick={() => setStatusFilter(filter)} onClick={() => setStatusFilter(filter)}
className={`whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition-colors ${ className={`whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
statusFilter === filter 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' : '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 <button
type="button" type="button"
onClick={() => navigate('/login', { replace: true })} 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')} {t('auth.login', 'Login')}
</button> </button>

View File

@@ -340,7 +340,7 @@ export default function Balance() {
{translatedDesc || method.description} {translatedDesc || method.description}
</div> </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.min_amount_kopeks / 100, 0)} {t('common.rangeTo', 'to')}{' '}
{formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div> </div>

View File

@@ -318,7 +318,7 @@ export default function Dashboard() {
) : ( ) : (
<Link <Link
to="/subscription/purchase" 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>{' '} <span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
@@ -381,7 +381,7 @@ export default function Dashboard() {
)} )}
<Link <Link
to="/subscription/purchase" 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>{' '} <span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {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]', '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 copied
? 'bg-success-500/20 text-success-400' ? '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" />} {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 && ( {gift.bot_claim_link && (
<a <a
href={gift.bot_claim_link} 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')} {t('landing.giftClaim.activateTelegram', 'Activate in Telegram')}
</a> </a>
@@ -303,7 +303,7 @@ export default function GiftClaim() {
className={cn( className={cn(
'flex w-full items-center justify-center gap-2 rounded-xl px-6 py-3.5 text-sm font-bold transition-all', '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 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', : '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]', '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 copied
? 'bg-success-500/20 text-success-400' ? '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 ? ( {copied ? (
@@ -236,7 +236,7 @@ function DeliveredState({
<button <button
type="button" type="button"
onClick={() => navigate('/')} 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')} {t('gift.backToDashboard', 'Back to dashboard')}
</button> </button>
@@ -303,7 +303,7 @@ function PendingActivationState({
<button <button
type="button" type="button"
onClick={() => navigate('/')} 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')} {t('gift.backToDashboard', 'Back to dashboard')}
</button> </button>
@@ -335,7 +335,7 @@ function FailedState() {
<button <button
type="button" type="button"
onClick={() => navigate('/gift')} 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')} {t('gift.tryAgain', 'Try again')}
</button> </button>
@@ -372,7 +372,7 @@ function PollErrorState() {
<button <button
type="button" type="button"
onClick={() => navigate('/')} 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')} {t('gift.backToDashboard', 'Back to dashboard')}
</button> </button>
@@ -406,7 +406,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
<button <button
type="button" type="button"
onClick={onRetry} 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')} {t('gift.retry', 'Retry')}
</button> </button>
@@ -436,7 +436,7 @@ function NoTokenState() {
<button <button
type="button" type="button"
onClick={() => navigate('/gift')} 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')} {t('gift.backToGift', 'Go back')}
</button> </button>

View File

@@ -231,7 +231,7 @@ function PeriodCard({
<span <span
className={cn( className={cn(
'rounded-md px-2 py-0.5 text-xs font-bold', '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}% -{period.discount_percent}%
@@ -366,7 +366,7 @@ function PaymentMethodCard({
className={cn( className={cn(
'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200', 'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200',
selectedSubOption === opt.id 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', : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50',
)} )}
> >
@@ -716,7 +716,7 @@ function BuyTabContent({
className={cn( 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', '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 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', : 'cursor-not-allowed bg-dark-800 text-dark-500',
)} )}
> >
@@ -833,7 +833,7 @@ function ActivateTabContent({ initialCode }: { initialCode?: string | null }) {
className={cn( className={cn(
'w-full max-w-sm rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200', 'w-full max-w-sm rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200',
code.trim() && !activateMutation.isPending 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', : 'cursor-not-allowed bg-dark-800 text-dark-500',
)} )}
> >
@@ -962,7 +962,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
<button <button
type="button" type="button"
onClick={handleShare} 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" /> <ExportIcon className="h-4 w-4" />
{t('gift.shareGift')} {t('gift.shareGift')}
@@ -1243,7 +1243,7 @@ export default function GiftSubscription() {
className={cn( className={cn(
'flex-1 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200', 'flex-1 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200',
activeTab === tab.id 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', : 'text-dark-400 hover:text-dark-200',
)} )}
> >

View File

@@ -838,7 +838,7 @@ export default function Info() {
onClick={() => setActiveTab(tab.id)} 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 ${ 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 activeTab === tab.id
? 'bg-accent-500 text-white' ? 'bg-accent-500 text-on-accent'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700' : 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`} }`}
> >

View File

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

View File

@@ -268,7 +268,7 @@ function SuccessState({
href={botLink} href={botLink}
target="_blank" target="_blank"
rel="noopener noreferrer" 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')} {t('landing.openBot')}
</a> </a>
@@ -492,7 +492,7 @@ function GiftPendingActivationState({
href={botLink} href={botLink}
target="_blank" target="_blank"
rel="noopener noreferrer" 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')} {t('landing.openBot')}
</a> </a>
@@ -545,7 +545,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
<button <button
type="button" type="button"
onClick={onRetry} 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')} {t('common.retry', 'Retry')}
</button> </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]', '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 copied
? 'bg-success-500/20 text-success-400' ? '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 ? ( {copied ? (

View File

@@ -122,7 +122,7 @@ function PeriodTabs({
className={cn( className={cn(
'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200', 'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200',
selectedDays === period.days 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', : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
)} )}
> >
@@ -409,7 +409,7 @@ function PaymentMethodCard({
className={cn( className={cn(
'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200', 'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200',
selectedSubOption === opt.id 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', : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
)} )}
> >
@@ -583,7 +583,7 @@ function SummaryCard({
className={cn( 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', '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 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', : 'cursor-not-allowed bg-dark-800 text-dark-500',
)} )}
> >
@@ -629,7 +629,7 @@ function SummaryCard({
className={cn( 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', '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 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', : '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"> <div className="flex flex-col items-center gap-4 px-5 py-5 sm:flex-row sm:justify-between">
{/* Left: badge + text */} {/* Left: badge + text */}
<div className="flex items-center gap-3"> <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}% -{discount.percent}%
</span> </span>
{discount.badge_text && ( {discount.badge_text && (

View File

@@ -226,7 +226,7 @@ export default function RenewSubscription() {
<button <button
onClick={() => handleRenew(selectedPeriod)} onClick={() => handleRenew(selectedPeriod)}
disabled={renewMutation.isPending} 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 {renewMutation.isPending
? t('common.processing', 'Обработка...') ? t('common.processing', 'Обработка...')

View File

@@ -515,7 +515,7 @@ export default function Subscription() {
</p> </p>
<button <button
onClick={() => navigate('/subscriptions')} 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', 'Мои подписки')} {t('subscription.backToList', 'Мои подписки')}
</button> </button>

View File

@@ -121,7 +121,7 @@ export default function SubscriptionPurchase() {
</p> </p>
<button <button
onClick={() => refetchOptions()} 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')} {t('common.retry')}
</button> </button>
@@ -322,7 +322,7 @@ export default function SubscriptionPurchase() {
</p> </p>
<button <button
onClick={() => refetchOptions()} 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')} {t('common.retry')}
</button> </button>

View File

@@ -35,7 +35,7 @@ function EmptyState({ onBuy }: { onBuy: () => void }) {
</p> </p>
<button <button
onClick={onBuy} 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', 'Купить подписку')} {t('subscriptions.buy', 'Купить подписку')}
</button> </button>
@@ -135,7 +135,7 @@ export default function Subscriptions() {
{!isLoading && subscriptions.length > 0 && !hasActivePaid && ( {!isLoading && subscriptions.length > 0 && !hasActivePaid && (
<button <button
onClick={() => navigate('/subscription/purchase')} 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" /> <PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
@@ -171,7 +171,7 @@ export default function Subscriptions() {
(Telegram-баг #605056/#605063). */} (Telegram-баг #605056/#605063). */}
<button <button
onClick={() => navigate('/subscription/purchase')} 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" /> <PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}

View File

@@ -507,7 +507,7 @@ export default function TopUpAmount() {
? 'cursor-not-allowed bg-dark-700 text-dark-500' ? 'cursor-not-allowed bg-dark-700 text-dark-500'
: isStarsMethod : 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-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 ? ( {isPending ? (

View File

@@ -82,7 +82,7 @@ export default function TopUpMethodSelect() {
{translatedDesc || method.description} {translatedDesc || method.description}
</div> </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.min_amount_kopeks / 100, 0)} {' '}
{formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div> </div>

View File

@@ -85,7 +85,7 @@ function SuccessState({ amountKopeks }: { amountKopeks: number | null }) {
<button <button
type="button" type="button"
onClick={handleGoToBalance} 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')} {t('balance.topUpResult.goToBalance')}
</button> </button>
@@ -162,7 +162,7 @@ function TimeoutState({ onRetry, onGoBack }: { onRetry: () => void; onGoBack: ()
<button <button
type="button" type="button"
onClick={onRetry} 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')} {t('common.retry')}
</button> </button>

View File

@@ -632,7 +632,7 @@ export default function Wheel() {
</button> </button>
<button <button
onClick={handleDirectStarsPay} 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 })} {t('wheel.payStars', { count: config.spin_cost_stars ?? 0 })}
</button> </button>

View File

@@ -342,6 +342,20 @@
--color-dark-850: var(--color-champagne-100) !important; --color-dark-850: var(--color-champagne-100) !important;
--color-dark-900: var(--color-champagne-100) !important; --color-dark-900: var(--color-champagne-100) !important;
--color-dark-950: var(--color-champagne-200) !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 */ /* Custom scrollbar - Dark */
@@ -578,7 +592,7 @@ img.twemoji {
} }
.btn-primary { .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 { .btn-secondary {

View File

@@ -1,9 +1,12 @@
import i18n from '../i18n';
/** /**
* Format traffic amount with appropriate unit (MB/GB/TB). * 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 { export function formatTraffic(gb: number): string {
if (gb >= 1000) return `${(gb / 1000).toFixed(1)} TB`; if (gb >= 1000) return `${(gb / 1000).toFixed(1)} ${i18n.t('common.units.tb', 'TB')}`;
if (gb >= 1) return `${gb.toFixed(1)} GB`; if (gb >= 1) return `${gb.toFixed(1)} ${i18n.t('common.units.gb', 'GB')}`;
return `${(gb * 1024).toFixed(0)} MB`; return `${(gb * 1024).toFixed(0)} ${i18n.t('common.units.mb', 'MB')}`;
} }

View File

@@ -45,6 +45,12 @@ export default {
900: withOpacity('--color-champagne-900'), 900: withOpacity('--color-champagne-900'),
950: withOpacity('--color-champagne-950'), 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 - dynamic color scheme
accent: { accent: {
50: withOpacity('--color-accent-50'), 50: withOpacity('--color-accent-50'),