From 7f68e2cd5ef510faf54537e8b93c4450be854589 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Thu, 11 Jun 2026 15:49:17 +0300 Subject: [PATCH] fix(theme): readable text on any operator palette + admin consistency pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Контраст-аудит (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 --- src/components/ErrorBoundary.tsx | 4 +- src/components/SuccessNotificationModal.tsx | 4 +- src/components/admin/AnalyticsTab.tsx | 6 +- src/components/admin/BackgroundEditor.tsx | 2 +- src/components/admin/BrandingTab.tsx | 2 +- src/components/admin/ButtonsTab.tsx | 4 +- src/components/admin/ColoredItemCombobox.tsx | 2 +- src/components/admin/MenuEditorTab.tsx | 4 +- src/components/admin/SettingInput.tsx | 4 +- .../admin/SortableSelectedMethodCard.tsx | 2 +- src/components/admin/ThemeTab.tsx | 2 +- .../admin/bulkActions/ActionModal.tsx | 4 +- .../admin/bulkActions/FloatingActionBar.tsx | 2 +- .../trafficUsage/filters/CountryFilter.tsx | 2 +- .../admin/trafficUsage/filters/NodeFilter.tsx | 2 +- .../trafficUsage/filters/PeriodSelector.tsx | 2 +- .../trafficUsage/filters/StatusFilter.tsx | 2 +- .../trafficUsage/filters/TariffFilter.tsx | 2 +- src/components/admin/userDetail/InfoTab.tsx | 4 +- .../admin/userDetail/SubscriptionTab.tsx | 2 +- .../admin/userDetail/TicketsTab.tsx | 2 +- src/components/dashboard/PendingGiftCard.tsx | 2 +- .../layout/AppShell/MobileBottomNav.tsx | 2 +- .../primitives/Button/Button.variants.ts | 2 +- .../purchase/TariffPickerGrid.tsx | 2 +- src/hooks/useThemeColors.ts | 98 ++++++++++++++++++- src/locales/en.json | 7 +- src/locales/fa.json | 7 +- src/locales/ru.json | 11 +-- src/locales/zh.json | 7 +- src/pages/AdminAuditLog.tsx | 6 +- src/pages/AdminBroadcastCreate.tsx | 6 +- src/pages/AdminBroadcastDetail.tsx | 2 +- src/pages/AdminBroadcasts.tsx | 2 +- src/pages/AdminCampaignCreate.tsx | 4 +- src/pages/AdminCampaignEdit.tsx | 4 +- src/pages/AdminCampaigns.tsx | 2 +- src/pages/AdminChannelSubscriptions.tsx | 6 +- src/pages/AdminEmailTemplates.tsx | 2 +- src/pages/AdminInfoPageEditor.tsx | 10 +- src/pages/AdminInfoPages.tsx | 6 +- src/pages/AdminLandingEditor.tsx | 2 +- src/pages/AdminLandings.tsx | 2 +- src/pages/AdminLegalPages.tsx | 12 +-- src/pages/AdminNews.tsx | 2 +- src/pages/AdminNewsCreate.tsx | 4 +- src/pages/AdminPartnerCampaignAssign.tsx | 2 +- src/pages/AdminPartnerCommission.tsx | 2 +- src/pages/AdminPaymentMethodEdit.tsx | 6 +- src/pages/AdminPaymentMethods.tsx | 2 +- src/pages/AdminPayments.tsx | 8 +- src/pages/AdminPinnedMessages.tsx | 2 +- src/pages/AdminPolicies.tsx | 2 +- src/pages/AdminPolicyEdit.tsx | 2 +- src/pages/AdminPromoGroups.tsx | 2 +- src/pages/AdminPromoOfferSend.tsx | 4 +- src/pages/AdminPromoOffers.tsx | 2 +- src/pages/AdminPromocodeStats.tsx | 2 +- src/pages/AdminPromocodes.tsx | 4 +- src/pages/AdminRoleAssign.tsx | 2 +- src/pages/AdminRoleEdit.tsx | 2 +- src/pages/AdminRoles.tsx | 2 +- src/pages/AdminServers.tsx | 2 +- src/pages/AdminSettings.tsx | 2 +- src/pages/AdminTariffCreate.tsx | 8 +- src/pages/AdminTariffs.tsx | 4 +- src/pages/AdminTickets.tsx | 4 +- src/pages/AdminUserDetail.tsx | 2 +- src/pages/AdminWithdrawalDetail.tsx | 2 +- src/pages/AdminWithdrawals.tsx | 2 +- src/pages/AutoLogin.tsx | 2 +- src/pages/Balance.tsx | 2 +- src/pages/Dashboard.tsx | 4 +- src/pages/GiftClaim.tsx | 6 +- src/pages/GiftResult.tsx | 14 +-- src/pages/GiftSubscription.tsx | 12 +-- src/pages/Info.tsx | 2 +- src/pages/Login.tsx | 4 +- src/pages/PurchaseSuccess.tsx | 8 +- src/pages/QuickPurchase.tsx | 10 +- src/pages/RenewSubscription.tsx | 2 +- src/pages/Subscription.tsx | 2 +- src/pages/SubscriptionPurchase.tsx | 4 +- src/pages/Subscriptions.tsx | 6 +- src/pages/TopUpAmount.tsx | 2 +- src/pages/TopUpMethodSelect.tsx | 2 +- src/pages/TopUpResult.tsx | 4 +- src/pages/Wheel.tsx | 2 +- src/styles/globals.css | 16 ++- src/utils/formatTraffic.ts | 11 ++- tailwind.config.js | 6 ++ 91 files changed, 287 insertions(+), 176 deletions(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index ab8206d..2bfb4e5 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -74,7 +74,7 @@ export class ErrorBoundary extends Component @@ -110,7 +110,7 @@ export class ErrorBoundary extends Component diff --git a/src/components/SuccessNotificationModal.tsx b/src/components/SuccessNotificationModal.tsx index 5af94fd..31cc36d 100644 --- a/src/components/SuccessNotificationModal.tsx +++ b/src/components/SuccessNotificationModal.tsx @@ -281,7 +281,7 @@ export default function SuccessNotificationModal() { {isSubscription && ( @@ -253,7 +253,7 @@ export function AnalyticsTab() { @@ -307,7 +307,7 @@ export function AnalyticsTab() { diff --git a/src/components/admin/BackgroundEditor.tsx b/src/components/admin/BackgroundEditor.tsx index 59c75e0..0aea7e7 100644 --- a/src/components/admin/BackgroundEditor.tsx +++ b/src/components/admin/BackgroundEditor.tsx @@ -68,7 +68,7 @@ export function BackgroundEditor() { 'w-full rounded-xl py-3 text-sm font-medium transition-colors', saveStatus === 'saved' ? 'bg-success-500/20 text-success-400' - : 'bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50', + : 'bg-accent-500 text-on-accent hover:bg-accent-600 disabled:opacity-50', )} > {saveStatus === 'saving' diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx index 6f13c67..0e9727f 100644 --- a/src/components/admin/BrandingTab.tsx +++ b/src/components/admin/BrandingTab.tsx @@ -168,7 +168,7 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { diff --git a/src/components/admin/ButtonsTab.tsx b/src/components/admin/ButtonsTab.tsx index 5d1ec29..c7b05d8 100644 --- a/src/components/admin/ButtonsTab.tsx +++ b/src/components/admin/ButtonsTab.tsx @@ -228,7 +228,7 @@ export function ButtonsTab() { ? 'bg-success-500 text-white' : cfg.style === 'danger' ? 'bg-error-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' }`} > {t(`admin.buttons.styles.${cfg.style}`)} @@ -321,7 +321,7 @@ export function ButtonsTab() { diff --git a/src/components/admin/ColoredItemCombobox.tsx b/src/components/admin/ColoredItemCombobox.tsx index d023f7b..8602494 100644 --- a/src/components/admin/ColoredItemCombobox.tsx +++ b/src/components/admin/ColoredItemCombobox.tsx @@ -346,7 +346,7 @@ export function ColoredItemCombobox({ type="button" onClick={handleCreate} disabled={isCreating} - className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50" + className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50" > {isCreating ? (
diff --git a/src/components/admin/MenuEditorTab.tsx b/src/components/admin/MenuEditorTab.tsx index fb8aeb4..56dbf11 100644 --- a/src/components/admin/MenuEditorTab.tsx +++ b/src/components/admin/MenuEditorTab.tsx @@ -72,7 +72,7 @@ function MaxPerRowSelector({ value, onChange }: MaxPerRowSelectorProps) { onClick={() => onChange(n)} className={`flex h-7 w-7 items-center justify-center rounded-lg text-xs font-semibold transition-all ${ value === n - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-700/50 text-dark-400 hover:bg-dark-600 hover:text-dark-300' }`} > @@ -788,7 +788,7 @@ export function MenuEditorTab() { diff --git a/src/components/admin/SettingInput.tsx b/src/components/admin/SettingInput.tsx index b930a42..1b8535f 100644 --- a/src/components/admin/SettingInput.tsx +++ b/src/components/admin/SettingInput.tsx @@ -122,7 +122,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps) @@ -647,7 +647,7 @@ export function ActionModal({ diff --git a/src/components/admin/bulkActions/FloatingActionBar.tsx b/src/components/admin/bulkActions/FloatingActionBar.tsx index 3808c06..8a05aed 100644 --- a/src/components/admin/bulkActions/FloatingActionBar.tsx +++ b/src/components/admin/bulkActions/FloatingActionBar.tsx @@ -208,7 +208,7 @@ export function FloatingActionBar({
@@ -475,7 +475,7 @@ export function InfoTab(props: InfoTabProps) { disabled={actionLoading} className={`rounded-lg px-3 py-2 text-sm font-medium transition-all disabled:opacity-50 ${ confirmingAction === 'resetTrial' - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-accent-500/15 text-accent-400 hover:bg-accent-500/25' }`} > diff --git a/src/components/admin/userDetail/SubscriptionTab.tsx b/src/components/admin/userDetail/SubscriptionTab.tsx index b6fec90..d633dc8 100644 --- a/src/components/admin/userDetail/SubscriptionTab.tsx +++ b/src/components/admin/userDetail/SubscriptionTab.tsx @@ -464,7 +464,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) { diff --git a/src/components/admin/userDetail/TicketsTab.tsx b/src/components/admin/userDetail/TicketsTab.tsx index 0dbd4f0..94a9db9 100644 --- a/src/components/admin/userDetail/TicketsTab.tsx +++ b/src/components/admin/userDetail/TicketsTab.tsx @@ -348,7 +348,7 @@ function ChatView({ onClick={onReply} disabled={!replyText.trim() || replySending} aria-label={t('admin.tickets.sendReply', 'Send reply')} - className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0" + className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0" > {replySending ? (
diff --git a/src/components/dashboard/PendingGiftCard.tsx b/src/components/dashboard/PendingGiftCard.tsx index a47f66a..2209bdb 100644 --- a/src/components/dashboard/PendingGiftCard.tsx +++ b/src/components/dashboard/PendingGiftCard.tsx @@ -57,7 +57,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro {/* Activate button */} {t('gift.pending.activate')} diff --git a/src/components/layout/AppShell/MobileBottomNav.tsx b/src/components/layout/AppShell/MobileBottomNav.tsx index f57ed25..21f27c5 100644 --- a/src/components/layout/AppShell/MobileBottomNav.tsx +++ b/src/components/layout/AppShell/MobileBottomNav.tsx @@ -80,7 +80,7 @@ export function MobileBottomNav({ onClick={handleNavClick} className={cn( 'relative flex min-w-[56px] flex-1 shrink-0 flex-col items-center justify-center rounded-2xl px-3 py-2.5 transition-all duration-200', - isActive(item.path) ? 'text-accent-400' : 'text-dark-500 hover:text-dark-300', + isActive(item.path) ? 'text-accent-400' : 'text-dark-400 hover:text-dark-200', )} > {isActive(item.path) && ( diff --git a/src/components/primitives/Button/Button.variants.ts b/src/components/primitives/Button/Button.variants.ts index 4893bc5..1d4c98f 100644 --- a/src/components/primitives/Button/Button.variants.ts +++ b/src/components/primitives/Button/Button.variants.ts @@ -13,7 +13,7 @@ export const buttonVariants = cva( variants: { variant: { primary: [ - 'bg-accent-500 text-white', + 'bg-accent-500 text-on-accent', 'hover:bg-accent-600', 'active:bg-accent-700', 'shadow-linear-sm hover:shadow-linear', diff --git a/src/components/subscription/purchase/TariffPickerGrid.tsx b/src/components/subscription/purchase/TariffPickerGrid.tsx index b1204ad..ee367fa 100644 --- a/src/components/subscription/purchase/TariffPickerGrid.tsx +++ b/src/components/subscription/purchase/TariffPickerGrid.tsx @@ -107,7 +107,7 @@ export function TariffPickerGrid({

diff --git a/src/hooks/useThemeColors.ts b/src/hooks/useThemeColors.ts index 88c32d2..470e398 100644 --- a/src/hooks/useThemeColors.ts +++ b/src/hooks/useThemeColors.ts @@ -54,6 +54,61 @@ function interpolateRgb( ); } +type Rgb = { r: number; g: number; b: number }; + +function mixRgb(rgb1: Rgb, rgb2: Rgb, factor: number): Rgb { + return { + r: Math.round(rgb1.r + (rgb2.r - rgb1.r) * factor), + g: Math.round(rgb1.g + (rgb2.g - rgb1.g) * factor), + b: Math.round(rgb1.b + (rgb2.b - rgb1.b) * factor), + }; +} + +// WCAG relative luminance +function relativeLuminance({ r, g, b }: Rgb): number { + const srgb = (v: number) => { + const c = v / 255; + return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }; + return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b); +} + +// WCAG contrast ratio between two colors +function contrastRatio(a: Rgb, b: Rgb): number { + const l1 = relativeLuminance(a); + const l2 = relativeLuminance(b); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +} + +/** + * Guarantee a minimum contrast for a TEXT color against its background. + * + * Operators pick arbitrary palette colors in the theme editor, and several + * tokens are additionally blended toward the surface (dark-500 = secondary + * text mixed 40% into the card color). Without a floor, hint/meta text + * regularly lands at a 1.5-2.5 contrast ratio and becomes unreadable. + * When the color already passes, it is returned untouched, so well-tuned + * palettes render byte-for-byte the same as before. + */ +function ensureReadable(fg: Rgb, towards: Rgb, bg: Rgb, minRatio: number): Rgb { + if (contrastRatio(fg, bg) >= minRatio) return fg; + for (let t = 0.1; t <= 1; t += 0.1) { + const mixed = mixRgb(fg, towards, t); + if (contrastRatio(mixed, bg) >= minRatio) return mixed; + } + return towards; +} + +// Black-or-white text for a given button/badge background: pick whichever +// side actually reads on top of it (operators may choose a light accent). +function onColorFor(bgTriplet: string): string { + const [r, g, b] = bgTriplet.split(',').map((x) => Number(x.trim())); + const bg = { r, g, b }; + const white = { r: 255, g: 255, b: 255 }; + const ink = { r: 15, g: 23, b: 42 }; + return contrastRatio(white, bg) >= contrastRatio(ink, bg) ? '255, 255, 255' : '15, 23, 42'; +} + // Apply theme colors as CSS variables (RGB format for Tailwind opacity support) export function applyThemeColors(colors: ThemeColors): void { const root = document.documentElement; @@ -70,6 +125,17 @@ export function applyThemeColors(colors: ThemeColors): void { const darkTextRgb = hexToRgb(colors.darkText); const darkTextSecRgb = hexToRgb(colors.darkTextSecondary); + // Contrast floors: secondary text must stay readable on the card surface + // regardless of the operator-chosen palette (AA 4.5 for dark-400, a softer + // 3.5 floor for the blended hint token dark-500). + const darkTextSecReadable = ensureReadable(darkTextSecRgb, darkTextRgb, darkSurfaceRgb, 5.0); + const darkHintReadable = ensureReadable( + mixRgb(darkTextSecRgb, darkSurfaceRgb, 0.4), + darkTextRgb, + darkSurfaceRgb, + 3.8, + ); + // Apply dark palette with actual user colors: // Text colors (light shades): 50-100 = primary text, 200-300 = mixed, 400 = secondary text root.style.setProperty( @@ -84,11 +150,14 @@ export function applyThemeColors(colors: ThemeColors): void { root.style.setProperty('--color-dark-300', interpolateRgb(darkTextRgb, darkTextSecRgb, 0.66)); root.style.setProperty( '--color-dark-400', - rgbToString(darkTextSecRgb.r, darkTextSecRgb.g, darkTextSecRgb.b), + rgbToString(darkTextSecReadable.r, darkTextSecReadable.g, darkTextSecReadable.b), ); // Transition colors (500-700): interpolate between secondary text and surface - root.style.setProperty('--color-dark-500', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.4)); + root.style.setProperty( + '--color-dark-500', + rgbToString(darkHintReadable.r, darkHintReadable.g, darkHintReadable.b), + ); root.style.setProperty('--color-dark-600', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.6)); root.style.setProperty('--color-dark-700', interpolateRgb(darkTextSecRgb, darkSurfaceRgb, 0.8)); @@ -122,11 +191,23 @@ export function applyThemeColors(colors: ThemeColors): void { root.style.setProperty('--color-champagne-300', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.2)); root.style.setProperty('--color-champagne-400', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.4)); - // Transition colors (500-600): between bg and text - root.style.setProperty('--color-champagne-500', interpolateRgb(lightBgRgb, lightTextSecRgb, 0.6)); + // Transition colors (500-600): between bg and text. + // Same contrast floors as the dark palette: champagne-600 backs dark-400 + // (secondary text) in the light theme, champagne-500 backs dark-500 (hints). + const lightHintReadable = ensureReadable( + mixRgb(lightBgRgb, lightTextSecRgb, 0.6), + lightTextRgb, + lightSurfaceRgb, + 3.8, + ); + const lightTextSecReadable = ensureReadable(lightTextSecRgb, lightTextRgb, lightSurfaceRgb, 5.0); + root.style.setProperty( + '--color-champagne-500', + rgbToString(lightHintReadable.r, lightHintReadable.g, lightHintReadable.b), + ); root.style.setProperty( '--color-champagne-600', - rgbToString(lightTextSecRgb.r, lightTextSecRgb.g, lightTextSecRgb.b), + rgbToString(lightTextSecReadable.r, lightTextSecReadable.g, lightTextSecReadable.b), ); // Text colors (700-950): secondary to primary text @@ -154,6 +235,13 @@ export function applyThemeColors(colors: ThemeColors): void { root.style.setProperty(`--color-error-${shade}`, errorPalette[shade]); } + // Readable text color on top of each status color (buttons, filled badges). + // Hardcoded white breaks the moment an operator picks a light accent. + root.style.setProperty('--color-on-accent', onColorFor(accentPalette[500])); + root.style.setProperty('--color-on-success', onColorFor(successPalette[500])); + root.style.setProperty('--color-on-warning', onColorFor(warningPalette[500])); + root.style.setProperty('--color-on-error', onColorFor(errorPalette[500])); + // Apply semantic colors (hex for direct use) root.style.setProperty('--color-dark-bg', colors.darkBackground); root.style.setProperty('--color-dark-surface', colors.darkSurface); diff --git a/src/locales/en.json b/src/locales/en.json index 3646c83..e284182 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -25,10 +25,9 @@ "copy": "Copy", "all": "All", "units": { + "tb": "TB", "gb": "GB", - "perGb": "/GB", - "days": "days", - "mo": "mo" + "mb": "MB" }, "retry": "Retry", "saving": "Saving...", @@ -1119,7 +1118,7 @@ "globalOpacity": "Opacity", "globalBlur": "Blur", "categoryCSS": "CSS", - "categoryCanvas": "Canvas", + "categoryCANVAS": "Canvas", "categorySVG": "SVG", "aurora": "Aurora", "auroraDesc": "Northern lights with smooth gradients", diff --git a/src/locales/fa.json b/src/locales/fa.json index a62e7f1..a041bbc 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -22,10 +22,9 @@ "currency": "تومان", "copy": "کپی", "units": { + "tb": "TB", "gb": "GB", - "perGb": "/GB", - "days": "روز", - "mo": "ماه" + "mb": "MB" }, "add": "افزودن", "refresh": "بازخوانی", @@ -1001,7 +1000,7 @@ "globalOpacity": "شفافیت", "globalBlur": "تاری", "categoryCSS": "CSS", - "categoryCanvas": "Canvas", + "categoryCANVAS": "Canvas", "categorySVG": "SVG", "aurora": "شفق قطبی", "auroraDesc": "شفق شمالی با گرادیان‌های نرم", diff --git a/src/locales/ru.json b/src/locales/ru.json index dcf9302..4131984 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -28,10 +28,9 @@ "saving": "Сохранение...", "understand": "Понятно", "units": { + "tb": "ТБ", "gb": "ГБ", - "perGb": "/ГБ", - "days": "дн", - "mo": "мес" + "mb": "МБ" }, "balance": "Баланс", "copied": "Скопировано", @@ -613,8 +612,8 @@ "unlimited": "Безлимит", "buyTrafficGb": "Купить {{gb}} ГБ", "buyUnlimited": "Купить Безлимит", - "manageServers": "Управление сквадами", - "manageServersTitle": "Управление сквадами", + "manageServers": "Управление локациями", + "manageServersTitle": "Управление локациями", "connectedDevices": "Подключённые устройства", "currentDeviceLimit": "Текущий лимит устройств", "decrementDevices": "Уменьшить кол-во устройств", @@ -1149,7 +1148,7 @@ "globalOpacity": "Прозрачность", "globalBlur": "Размытие", "categoryCSS": "CSS", - "categoryCanvas": "Canvas", + "categoryCANVAS": "Canvas", "categorySVG": "SVG", "aurora": "Aurora", "auroraDesc": "Северное сияние с плавными переливами", diff --git a/src/locales/zh.json b/src/locales/zh.json index f35e9f2..59fb2cc 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -28,10 +28,9 @@ "understand": "明白了", "collapse": "收起", "units": { + "tb": "TB", "gb": "GB", - "perGb": "/GB", - "days": "天", - "mo": "月" + "mb": "MB" }, "balance": "余额", "copied": "已复制", @@ -1001,7 +1000,7 @@ "globalOpacity": "透明度", "globalBlur": "模糊", "categoryCSS": "CSS", - "categoryCanvas": "Canvas", + "categoryCANVAS": "Canvas", "categorySVG": "SVG", "aurora": "极光", "auroraDesc": "柔和渐变的北极光", diff --git a/src/pages/AdminAuditLog.tsx b/src/pages/AdminAuditLog.tsx index 0a3f533..3cd3886 100644 --- a/src/pages/AdminAuditLog.tsx +++ b/src/pages/AdminAuditLog.tsx @@ -545,7 +545,7 @@ export default function AdminAuditLog() { handleExport(); }} disabled={exporting} - className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50" + className="flex items-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50" > @@ -614,7 +614,7 @@ export default function AdminAuditLog() { @@ -734,7 +734,7 @@ export default function AdminAuditLog() {
diff --git a/src/pages/AdminBroadcastCreate.tsx b/src/pages/AdminBroadcastCreate.tsx index 8ed513a..529eb5e 100644 --- a/src/pages/AdminBroadcastCreate.tsx +++ b/src/pages/AdminBroadcastCreate.tsx @@ -702,7 +702,7 @@ export default function AdminBroadcastCreate() { onClick={() => toggleButton(button.key)} className={`rounded-lg px-3 py-2 text-sm transition-colors ${ selectedButtons.includes(button.key) - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > @@ -770,7 +770,7 @@ export default function AdminBroadcastCreate() { onClick={() => setNewButtonActionType('callback')} className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${ newButtonActionType === 'callback' - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > @@ -781,7 +781,7 @@ export default function AdminBroadcastCreate() { onClick={() => setNewButtonActionType('url')} className={`flex-1 rounded-lg px-3 py-2 text-sm transition-colors ${ newButtonActionType === 'url' - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'border border-dark-700 bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > diff --git a/src/pages/AdminBroadcastDetail.tsx b/src/pages/AdminBroadcastDetail.tsx index e23fd46..1ee5c6c 100644 --- a/src/pages/AdminBroadcastDetail.tsx +++ b/src/pages/AdminBroadcastDetail.tsx @@ -155,7 +155,7 @@ export default function AdminBroadcastDetail() {

{t('admin.broadcasts.notFound')}

diff --git a/src/pages/AdminBroadcasts.tsx b/src/pages/AdminBroadcasts.tsx index 09f752f..fd02b82 100644 --- a/src/pages/AdminBroadcasts.tsx +++ b/src/pages/AdminBroadcasts.tsx @@ -117,7 +117,7 @@ export default function AdminBroadcasts() { @@ -1108,7 +1108,7 @@ export default function AdminInfoPageEditor() { className={cn( 'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors', displayMode === mode - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100', )} > @@ -1136,7 +1136,7 @@ export default function AdminInfoPageEditor() { pageType === pt ? pt === 'faq' ? 'bg-warning-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' : 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100', )} > @@ -1194,7 +1194,7 @@ export default function AdminInfoPageEditor() { className={cn( 'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors', activeLocale === loc - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100', )} > @@ -1415,7 +1415,7 @@ export default function AdminInfoPageEditor() { diff --git a/src/pages/AdminInfoPages.tsx b/src/pages/AdminInfoPages.tsx index 89bf1f5..ec8ef83 100644 --- a/src/pages/AdminInfoPages.tsx +++ b/src/pages/AdminInfoPages.tsx @@ -238,7 +238,7 @@ export default function AdminInfoPages() { haptic.buttonPress(); navigate('/admin/info-pages/create?type=faq'); }} - className="flex min-h-[44px] items-center gap-2 rounded-lg bg-warning-500/80 px-4 py-2.5 text-white transition-colors hover:bg-warning-500" + className="flex min-h-[44px] items-center gap-2 rounded-lg bg-warning-500 px-4 py-2.5 text-on-warning transition-colors hover:bg-warning-400" aria-label={t('admin.infoPages.createFaq')} > @@ -249,7 +249,7 @@ export default function AdminInfoPages() { haptic.buttonPress(); navigate('/admin/info-pages/create'); }} - className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-white transition-colors hover:bg-accent-600" + className="flex min-h-[44px] items-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-on-accent transition-colors hover:bg-accent-600" aria-label={t('admin.infoPages.create')} > @@ -268,7 +268,7 @@ export default function AdminInfoPages() { className={cn( 'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors', activeFilter === tab - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100', )} > diff --git a/src/pages/AdminLandingEditor.tsx b/src/pages/AdminLandingEditor.tsx index 0a84acc..c260ba4 100644 --- a/src/pages/AdminLandingEditor.tsx +++ b/src/pages/AdminLandingEditor.tsx @@ -588,7 +588,7 @@ export default function AdminLandingEditor() { @@ -321,7 +321,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi @@ -604,7 +604,7 @@ function FaqEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => void @@ -912,7 +912,7 @@ export default function AdminNewsCreate() { diff --git a/src/pages/AdminPartnerCampaignAssign.tsx b/src/pages/AdminPartnerCampaignAssign.tsx index 575f83b..fe53156 100644 --- a/src/pages/AdminPartnerCampaignAssign.tsx +++ b/src/pages/AdminPartnerCampaignAssign.tsx @@ -68,7 +68,7 @@ export default function AdminPartnerCampaignAssign() {
diff --git a/src/pages/AdminPartnerCommission.tsx b/src/pages/AdminPartnerCommission.tsx index c840063..1eccf40 100644 --- a/src/pages/AdminPartnerCommission.tsx +++ b/src/pages/AdminPartnerCommission.tsx @@ -90,7 +90,7 @@ export default function AdminPartnerCommission() { Number(commissionValue) < 1 || Number(commissionValue) > 100 } - className="flex-1 rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50" + className="flex-1 rounded-lg bg-accent-500 px-4 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50" > {updateMutation.isPending ? t('common.saving') : t('common.save')} diff --git a/src/pages/AdminPaymentMethodEdit.tsx b/src/pages/AdminPaymentMethodEdit.tsx index 67abd17..9201b7e 100644 --- a/src/pages/AdminPaymentMethodEdit.tsx +++ b/src/pages/AdminPaymentMethodEdit.tsx @@ -301,7 +301,9 @@ export default function AdminPaymentMethodEdit() { {opt.name}
{enabled && } @@ -493,7 +495,7 @@ export default function AdminPaymentMethodEdit() { {group.name}
{selected && } diff --git a/src/pages/AdminPaymentMethods.tsx b/src/pages/AdminPaymentMethods.tsx index 8667dd1..922154e 100644 --- a/src/pages/AdminPaymentMethods.tsx +++ b/src/pages/AdminPaymentMethods.tsx @@ -218,7 +218,7 @@ export default function AdminPaymentMethods() { )}
-

{t('admin.paymentMethods.title')}

+

{t('admin.paymentMethods.title')}

{t('admin.paymentMethods.description')}

diff --git a/src/pages/AdminPayments.tsx b/src/pages/AdminPayments.tsx index 368cc73..fe1713b 100644 --- a/src/pages/AdminPayments.tsx +++ b/src/pages/AdminPayments.tsx @@ -184,7 +184,7 @@ export default function AdminPayments() { )}
-

{t('admin.payments.title')}

+

{t('admin.payments.title')}

{t('admin.payments.description')}

@@ -239,7 +239,7 @@ export default function AdminPayments() { onClick={() => setStatusFilter(option.value)} className={`rounded-lg px-3 py-1.5 text-sm transition-all ${ statusFilter === option.value - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > @@ -256,7 +256,7 @@ export default function AdminPayments() { onClick={() => handlePeriodChange(option.value)} className={`rounded-lg px-3 py-1.5 text-sm transition-all ${ periodFilter === option.value - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > @@ -267,7 +267,7 @@ export default function AdminPayments() { onClick={() => handlePeriodChange('custom')} className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-all ${ periodFilter === 'custom' - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > diff --git a/src/pages/AdminPinnedMessages.tsx b/src/pages/AdminPinnedMessages.tsx index 1d13b05..aa6a87d 100644 --- a/src/pages/AdminPinnedMessages.tsx +++ b/src/pages/AdminPinnedMessages.tsx @@ -269,7 +269,7 @@ export default function AdminPinnedMessages() { diff --git a/src/pages/AdminPromoGroups.tsx b/src/pages/AdminPromoGroups.tsx index b5aa936..03bcd8b 100644 --- a/src/pages/AdminPromoGroups.tsx +++ b/src/pages/AdminPromoGroups.tsx @@ -65,7 +65,7 @@ export default function AdminPromoGroups() {
@@ -441,7 +441,7 @@ export default function AdminPromoOfferSend() { {selectedTemplate.message_text}
- + {selectedTemplate.button_text}
diff --git a/src/pages/AdminPromoOffers.tsx b/src/pages/AdminPromoOffers.tsx index c94a0b9..11d60e7 100644 --- a/src/pages/AdminPromoOffers.tsx +++ b/src/pages/AdminPromoOffers.tsx @@ -93,7 +93,7 @@ export default function AdminPromoOffers() {
)}
-

{t('admin.promocodes.title')}

+

{t('admin.promocodes.title')}

{t('admin.promocodes.subtitle')}

diff --git a/src/pages/AdminRoles.tsx b/src/pages/AdminRoles.tsx index 746e235..69a23a5 100644 --- a/src/pages/AdminRoles.tsx +++ b/src/pages/AdminRoles.tsx @@ -81,7 +81,7 @@ export default function AdminRoles() { )} -

{t('admin.settings.title')}

+

{t('admin.settings.title')}

@@ -691,7 +691,7 @@ export default function AdminTariffCreate() { isSelected ? isDaily ? 'bg-warning-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' : 'bg-dark-600' }`} > @@ -740,7 +740,7 @@ export default function AdminTariffCreate() { isSelected ? isDaily ? 'bg-warning-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' : 'bg-dark-600' }`} > @@ -1055,7 +1055,7 @@ export default function AdminTariffCreate() { isSelected ? isDaily ? 'bg-warning-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' : 'bg-dark-600' }`} > diff --git a/src/pages/AdminTariffs.tsx b/src/pages/AdminTariffs.tsx index dc139c2..4c337cc 100644 --- a/src/pages/AdminTariffs.tsx +++ b/src/pages/AdminTariffs.tsx @@ -307,7 +307,7 @@ export default function AdminTariffs() { )}
-

{t('admin.tariffs.title')}

+

{t('admin.tariffs.title')}

{t('admin.tariffs.subtitle')}

@@ -328,7 +328,7 @@ export default function AdminTariffs() { )} )} -

- {t('admin.tickets.title')} -

+

{t('admin.tickets.title')}

diff --git a/src/pages/AdminWithdrawalDetail.tsx b/src/pages/AdminWithdrawalDetail.tsx index b292bc3..e641d53 100644 --- a/src/pages/AdminWithdrawalDetail.tsx +++ b/src/pages/AdminWithdrawalDetail.tsx @@ -364,7 +364,7 @@ export default function AdminWithdrawalDetail() { diff --git a/src/pages/Balance.tsx b/src/pages/Balance.tsx index 729355b..2943519 100644 --- a/src/pages/Balance.tsx +++ b/src/pages/Balance.tsx @@ -340,7 +340,7 @@ export default function Balance() { {translatedDesc || method.description} )} -
+
{formatAmount(method.min_amount_kopeks / 100, 0)} {t('common.rangeTo', 'to')}{' '} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 41557a5..0d0e254 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -318,7 +318,7 @@ export default function Dashboard() { ) : ( +{' '} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} @@ -381,7 +381,7 @@ export default function Dashboard() { )} +{' '} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} diff --git a/src/pages/GiftClaim.tsx b/src/pages/GiftClaim.tsx index 902ee95..2a4c560 100644 --- a/src/pages/GiftClaim.tsx +++ b/src/pages/GiftClaim.tsx @@ -189,7 +189,7 @@ export default function GiftClaim() { 'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3 text-sm font-bold transition-all active:scale-[0.98]', copied ? 'bg-success-500/20 text-success-400' - : 'bg-accent-500 text-white hover:bg-accent-400', + : 'bg-accent-500 text-on-accent hover:bg-accent-400', )} > {copied ? : } @@ -264,7 +264,7 @@ export default function GiftClaim() { {gift.bot_claim_link && ( {t('landing.giftClaim.activateTelegram', 'Activate in Telegram')} @@ -303,7 +303,7 @@ export default function GiftClaim() { className={cn( 'flex w-full items-center justify-center gap-2 rounded-xl px-6 py-3.5 text-sm font-bold transition-all', isValidEmail(email) && !claimMutation.isPending - ? 'bg-accent-500 text-white hover:bg-accent-400 active:scale-[0.98]' + ? 'bg-accent-500 text-on-accent hover:bg-accent-400 active:scale-[0.98]' : 'cursor-not-allowed bg-dark-800 text-dark-500', )} > diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx index b083f1a..a77c549 100644 --- a/src/pages/GiftResult.tsx +++ b/src/pages/GiftResult.tsx @@ -156,7 +156,7 @@ function CodeOnlySuccessState({ 'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3.5 text-sm font-bold transition-all duration-200 active:scale-[0.98]', copied ? 'bg-success-500/20 text-success-400' - : 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400', + : 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400', )} > {copied ? ( @@ -236,7 +236,7 @@ function DeliveredState({ @@ -303,7 +303,7 @@ function PendingActivationState({ @@ -335,7 +335,7 @@ function FailedState() { @@ -372,7 +372,7 @@ function PollErrorState() { @@ -406,7 +406,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) { @@ -436,7 +436,7 @@ function NoTokenState() { diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx index 92c6da5..57ff132 100644 --- a/src/pages/GiftSubscription.tsx +++ b/src/pages/GiftSubscription.tsx @@ -231,7 +231,7 @@ function PeriodCard({ -{period.discount_percent}% @@ -366,7 +366,7 @@ function PaymentMethodCard({ className={cn( 'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200', selectedSubOption === opt.id - ? 'bg-accent-500 text-white shadow-sm shadow-accent-500/25' + ? 'bg-accent-500 text-on-accent shadow-sm shadow-accent-500/25' : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50', )} > @@ -716,7 +716,7 @@ function BuyTabContent({ className={cn( 'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200', canSubmit && !purchaseMutation.isPending - ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' + ? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' : 'cursor-not-allowed bg-dark-800 text-dark-500', )} > @@ -833,7 +833,7 @@ function ActivateTabContent({ initialCode }: { initialCode?: string | null }) { className={cn( 'w-full max-w-sm rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200', code.trim() && !activateMutation.isPending - ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 active:scale-[0.98]' + ? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 active:scale-[0.98]' : 'cursor-not-allowed bg-dark-800 text-dark-500', )} > @@ -962,7 +962,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) { @@ -641,7 +641,7 @@ function GiftLinkShareState({ 'flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3.5 text-sm font-bold transition-all duration-200 active:scale-[0.98]', copied ? 'bg-success-500/20 text-success-400' - : 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400', + : 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400', )} > {copied ? ( diff --git a/src/pages/QuickPurchase.tsx b/src/pages/QuickPurchase.tsx index a6fa30f..ebd236f 100644 --- a/src/pages/QuickPurchase.tsx +++ b/src/pages/QuickPurchase.tsx @@ -122,7 +122,7 @@ function PeriodTabs({ className={cn( 'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200', selectedDays === period.days - ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25' + ? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25' : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100', )} > @@ -409,7 +409,7 @@ function PaymentMethodCard({ className={cn( 'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200', selectedSubOption === opt.id - ? 'bg-accent-500 text-white shadow-sm shadow-accent-500/25' + ? 'bg-accent-500 text-on-accent shadow-sm shadow-accent-500/25' : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100', )} > @@ -583,7 +583,7 @@ function SummaryCard({ className={cn( 'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200', canSubmit && !isSubmitting - ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' + ? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' : 'cursor-not-allowed bg-dark-800 text-dark-500', )} > @@ -629,7 +629,7 @@ function SummaryCard({ className={cn( 'flex w-full items-center justify-center gap-2 rounded-2xl px-6 py-4 text-base font-semibold transition-all duration-200', canSubmit && !isSubmitting - ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' + ? 'bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-accent-500/40 active:scale-[0.98]' : 'cursor-not-allowed bg-dark-800 text-dark-500', )} > @@ -734,7 +734,7 @@ function DiscountBanner({
{/* Left: badge + text */}
- + -{discount.percent}% {discount.badge_text && ( diff --git a/src/pages/RenewSubscription.tsx b/src/pages/RenewSubscription.tsx index 22c87de..8b9ea7e 100644 --- a/src/pages/RenewSubscription.tsx +++ b/src/pages/RenewSubscription.tsx @@ -226,7 +226,7 @@ export default function RenewSubscription() { diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index d5e1387..d9dc868 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -121,7 +121,7 @@ export default function SubscriptionPurchase() {

@@ -322,7 +322,7 @@ export default function SubscriptionPurchase() {

diff --git a/src/pages/Subscriptions.tsx b/src/pages/Subscriptions.tsx index 03d33b4..be8f65f 100644 --- a/src/pages/Subscriptions.tsx +++ b/src/pages/Subscriptions.tsx @@ -35,7 +35,7 @@ function EmptyState({ onBuy }: { onBuy: () => void }) {

@@ -135,7 +135,7 @@ export default function Subscriptions() { {!isLoading && subscriptions.length > 0 && !hasActivePaid && (
)} -
+
{formatAmount(method.min_amount_kopeks / 100, 0)} –{' '} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
diff --git a/src/pages/TopUpResult.tsx b/src/pages/TopUpResult.tsx index 0581c2f..e95f3e6 100644 --- a/src/pages/TopUpResult.tsx +++ b/src/pages/TopUpResult.tsx @@ -85,7 +85,7 @@ function SuccessState({ amountKopeks }: { amountKopeks: number | null }) { @@ -162,7 +162,7 @@ function TimeoutState({ onRetry, onGoBack }: { onRetry: () => void; onGoBack: () diff --git a/src/pages/Wheel.tsx b/src/pages/Wheel.tsx index 0a742f1..0070ff1 100644 --- a/src/pages/Wheel.tsx +++ b/src/pages/Wheel.tsx @@ -632,7 +632,7 @@ export default function Wheel() { diff --git a/src/styles/globals.css b/src/styles/globals.css index 4e783fe..bb9ae21 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -342,6 +342,20 @@ --color-dark-850: var(--color-champagne-100) !important; --color-dark-900: var(--color-champagne-100) !important; --color-dark-950: var(--color-champagne-200) !important; + + /* Статусные палитры генерируются под тёмную тему: текстовые оттенки + 300/400 (зелёные суммы, жёлтая «Админка», бейджи) на светлом фоне + дают контраст 1.3–1.6. В светлой теме подменяем их тёмными шейдами — + зеркально ремапу серых выше. Фоновые употребления (bg-*-500/15 и т.п.) + используют 500+ и не затрагиваются. */ + --color-accent-300: var(--color-accent-700) !important; + --color-accent-400: var(--color-accent-700) !important; + --color-success-300: var(--color-success-700) !important; + --color-success-400: var(--color-success-700) !important; + --color-warning-300: var(--color-warning-700) !important; + --color-warning-400: var(--color-warning-700) !important; + --color-error-300: var(--color-error-700) !important; + --color-error-400: var(--color-error-700) !important; } /* Custom scrollbar - Dark */ @@ -578,7 +592,7 @@ img.twemoji { } .btn-primary { - @apply btn bg-accent-500 text-white shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-xl hover:shadow-accent-500/30 focus-visible:ring-accent-500 active:bg-accent-600; + @apply btn bg-accent-500 text-on-accent shadow-lg shadow-accent-500/25 hover:bg-accent-400 hover:shadow-xl hover:shadow-accent-500/30 focus-visible:ring-accent-500 active:bg-accent-600; } .btn-secondary { diff --git a/src/utils/formatTraffic.ts b/src/utils/formatTraffic.ts index 34fd6a4..bf2c94b 100644 --- a/src/utils/formatTraffic.ts +++ b/src/utils/formatTraffic.ts @@ -1,9 +1,12 @@ +import i18n from '../i18n'; + /** * Format traffic amount with appropriate unit (MB/GB/TB). - * Matches the prototype's formatBytes logic. + * Units come from i18n so RU shows «ГБ» consistently with the rest of the UI + * (templates previously mixed latin "GB" with localized «ГБ» on one screen). */ export function formatTraffic(gb: number): string { - if (gb >= 1000) return `${(gb / 1000).toFixed(1)} TB`; - if (gb >= 1) return `${gb.toFixed(1)} GB`; - return `${(gb * 1024).toFixed(0)} MB`; + if (gb >= 1000) return `${(gb / 1000).toFixed(1)} ${i18n.t('common.units.tb', 'TB')}`; + if (gb >= 1) return `${gb.toFixed(1)} ${i18n.t('common.units.gb', 'GB')}`; + return `${(gb * 1024).toFixed(0)} ${i18n.t('common.units.mb', 'MB')}`; } diff --git a/tailwind.config.js b/tailwind.config.js index fb1410b..cae11e8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -45,6 +45,12 @@ export default { 900: withOpacity('--color-champagne-900'), 950: withOpacity('--color-champagne-950'), }, + // Readable text on top of status-colored fills (computed from the + // operator palette in useThemeColors — black or white, whichever reads) + 'on-accent': withOpacity('--color-on-accent'), + 'on-success': withOpacity('--color-on-success'), + 'on-warning': withOpacity('--color-on-warning'), + 'on-error': withOpacity('--color-on-error'), // Accent - dynamic color scheme accent: { 50: withOpacity('--color-accent-50'),