redesign(ui): спортивная палитра Strava + полная адаптивность
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled

== Цветовая гамма ==
Сменили esports cyan+violet на спортивную Strava-палитру:
- accent: orange-500 (#F97316) — энергия, движение, "GO"
- accent-2: rose-500 (#F43F5E) — интенсивность, gradient pair
- victory: lime-500 — личный рекорд / готово
- defeat: red-600
- xp: amber-500
Dark theme переведена с холодного navy на тёплый графит,
light theme очищена в "athletic paper" feel.

== Репозиционирование текста ==
Убрали esports-сленг в пользу спортивной лексики:
- Sidebar: "Play hard · Train harder" → "Move every day"
- Sidebar footer: "GSI-трекинг матчей" → "Трекинг активности"
- Dashboard микрозаголовок: "Mission control" → "Тренировка дня"
- Dashboard HUD: "Cooldown / READY" → "До следующего / СЕЙЧАС",
  "Game tracking / LIVE / OFF" → "Трекинг матчей / LIVE / OFF"
- Exercises: "Loadout" → "Программа"
- Games: "Game integrations" → "Трекинг матчей"
- Challenges: "Match rules" → "Правила за матч"
- Settings: "Config" → "Конфигурация"
- ExerciseCard: "Cooldown / PAUSED" → "Через / пауза"
- ReminderApp: "Cooldown ready" → "Время тренировки",
  "Время размяться" → "Двигайся",
  "Next drop" → "Следующее",
  "Victory · упражнения заработаны" → "Победа · тренировка заработана",
  "Defeat · но тело — нет" → "Проигрыш · но тело не сдаётся",
  "all clear" → "готово", "Total / reps" → "Всего / повторов",
  "GG" → "Готово"

== Адаптивная вёрстка ==
- App.tsx: state mobileNavOpen, авто-закрытие drawer на route change
- Sidebar: три режима через CSS breakpoints —
  * lg+ (≥1024px): полная ширина w-60 с лейблами
  * md (768-1023px): icon-only w-16 с title-тултипами
  * <md (<768px): скрыта, открывается drawer-ом по hamburger
- Titlebar: hamburger button слева на <md, title скрывается на <sm
- Все hero-блоки страниц: flex-col на sm, flex-row sm:items-end
  sm:justify-between с stack gap-4
- Padding страниц: p-4 sm:p-6 lg:p-8 вместо p-8
- Hero h1: text-3xl sm:text-4xl
- Dashboard HUD strip: grid-cols-2 lg:grid-cols-4 (было 1/2/4)
- Action buttons в карточках/строках: opacity-100 lg:opacity-0
  lg:group-hover (на узких экранах всегда видны)
- GameRow buttons: flex-wrap для длинных лейблов
- Dashboard challenges shortcut: hint hidden sm:block
- Sidebar mobile drawer: framer-motion слайд + backdrop с blur

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AnRil
2026-05-17 12:50:20 +07:00
parent 92e15e69a3
commit cec146ae3a
11 changed files with 235 additions and 109 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { HashRouter, Route, Routes, useLocation } from 'react-router-dom'
import { Sidebar } from './components/Sidebar'
import { Titlebar } from './components/Titlebar'
import Dashboard from './pages/Dashboard'
@@ -11,6 +11,7 @@ import { subscribeToBackend, useAppStore } from './store/appStore'
export default function App(): JSX.Element {
const hydrated = useAppStore((s) => s.hydrated)
const [mobileNavOpen, setMobileNavOpen] = useState(false)
useEffect(() => {
const unsub = subscribeToBackend()
@@ -20,18 +21,18 @@ export default function App(): JSX.Element {
return (
<HashRouter>
<div className="h-screen w-screen flex flex-col bg-bg">
<Titlebar title="Exercise Reminder" />
<Titlebar
title="Exercise Reminder"
onMenuClick={() => setMobileNavOpen(true)}
/>
<div className="flex-1 flex overflow-hidden">
<Sidebar />
<main className="flex-1 overflow-hidden">
<Sidebar
mobileOpen={mobileNavOpen}
onMobileClose={() => setMobileNavOpen(false)}
/>
<main className="flex-1 overflow-hidden min-w-0">
{hydrated ? (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/exercises" element={<Exercises />} />
<Route path="/games" element={<GamesPage />} />
<Route path="/challenges" element={<ChallengesPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
<RoutesWithCloseOnNav onClose={() => setMobileNavOpen(false)} />
) : (
<div className="p-8 text-muted">Загрузка</div>
)}
@@ -41,3 +42,21 @@ export default function App(): JSX.Element {
</HashRouter>
)
}
// Close mobile drawer whenever the route changes.
function RoutesWithCloseOnNav({ onClose }: { onClose: () => void }): JSX.Element {
const location = useLocation()
useEffect(() => {
onClose()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname])
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/exercises" element={<Exercises />} />
<Route path="/games" element={<GamesPage />} />
<Route path="/challenges" element={<ChallengesPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
)
}

View File

@@ -123,7 +123,7 @@ function ExerciseReminder({
<div className="reminder-shell flex flex-col h-full hud-scanlines">
<div className="titlebar-drag h-9 px-3 flex items-center justify-between">
<div className="text-[10px] uppercase tracking-[0.2em] text-accent font-display font-semibold inline-flex items-center gap-1.5 px-2">
<Flame size={11} /> Cooldown ready
<Flame size={11} /> Время тренировки
</div>
<button
onClick={onClose}
@@ -158,7 +158,7 @@ function ExerciseReminder({
</motion.div>
<div className="text-[10px] uppercase tracking-[0.28em] text-muted font-semibold">
Время размяться
Двигайся
</div>
<h1 className="font-display text-3xl font-bold mt-2 mb-3 uppercase tracking-wide">
{exercise.name}
@@ -176,7 +176,7 @@ function ExerciseReminder({
</div>
<div className="text-[11px] text-muted mt-4 inline-flex items-center gap-1.5">
<Clock size={10} />
Next drop через {formatInterval(exercise.intervalMinutes)}
Следующее через {formatInterval(exercise.intervalMinutes)}
</div>
</div>
@@ -280,9 +280,9 @@ function MatchSummaryView({
</motion.div>
<h1 className="font-display font-bold text-xl uppercase tracking-wide">
{won
? 'Victory · упражнения заработаны'
? 'Победа · тренировка заработана'
: lost
? 'Defeat · но тело — нет'
? 'Проигрыш · но тело не сдаётся'
: 'Матч завершён'}
</h1>
<p className="text-[11px] text-muted mt-1.5">
@@ -293,7 +293,7 @@ function MatchSummaryView({
челлендж{summary.results.length === 1 ? '' : 'а'} ·{' '}
{allDone ? (
<span className="text-victory font-semibold uppercase tracking-wider">
all clear
готово
</span>
) : (
<span className="text-accent font-bold font-mono-num">
@@ -316,11 +316,11 @@ function MatchSummaryView({
<div className="px-6 pb-6 pt-3 flex items-center gap-3 border-t border-border/40">
<div className="flex-1 text-[11px] text-muted uppercase tracking-[0.15em] font-semibold">
Total ·{' '}
Всего ·{' '}
<span className="text-gradient-brand font-mono-num text-base font-bold tracking-normal">
{totalReps}
</span>{' '}
reps
повторов
</div>
<button
onClick={onClose}
@@ -331,7 +331,7 @@ function MatchSummaryView({
>
{allDone ? (
<>
<Check size={16} /> GG
<Check size={16} /> Готово
</>
) : (
'Позже'

View File

@@ -130,7 +130,7 @@ export function ExerciseCard({
<div className="relative">
<div className="flex items-baseline justify-between mb-1.5">
<span className="text-[10px] uppercase tracking-[0.18em] text-muted font-semibold">
Cooldown
Через
</span>
<span
className={[
@@ -138,7 +138,7 @@ export function ExerciseCard({
isDue ? 'text-accent' : 'text-text'
].join(' ')}
>
{exercise.enabled ? formatCountdown(ms) : 'PAUSED'}
{exercise.enabled ? formatCountdown(ms) : 'пауза'}
</span>
</div>
<div className="h-1.5 rounded-full bg-surface-elevated/80 overflow-hidden">
@@ -153,7 +153,7 @@ export function ExerciseCard({
</div>
</div>
<div className="relative flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="relative flex items-center gap-2 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
<button
onClick={onMarkDone}
className="flex-1 h-9 rounded-lg bg-victory/15 hover:bg-victory/25 text-victory text-xs font-semibold inline-flex items-center justify-center gap-1.5 transition-colors"

View File

@@ -1,11 +1,13 @@
import { NavLink } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion'
import {
LayoutDashboard,
ListChecks,
Gamepad2,
Target,
Settings as SettingsIcon,
Dumbbell
Dumbbell,
X
} from 'lucide-react'
const links = [
@@ -16,42 +18,111 @@ const links = [
{ to: '/settings', label: 'Настройки', icon: SettingsIcon }
]
export function Sidebar(): JSX.Element {
type Props = {
mobileOpen?: boolean
onMobileClose?: () => void
}
export function Sidebar({ mobileOpen = false, onMobileClose }: Props): JSX.Element {
return (
<aside className="w-60 shrink-0 border-r border-border/60 bg-surface/40 backdrop-blur-sm flex flex-col relative">
<>
{/* Desktop sidebar: hidden on <md, icon-only on md, full on lg+ */}
<aside className="hidden md:flex w-16 lg:w-60 shrink-0 border-r border-border/60 bg-surface/40 backdrop-blur-sm flex-col relative">
<SidebarContent compact />
</aside>
{/* Mobile drawer */}
<AnimatePresence>
{mobileOpen && (
<motion.div
className="md:hidden fixed inset-0 z-50 flex"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onMobileClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
<motion.aside
className="relative w-64 max-w-[80vw] h-full border-r border-border/60 bg-surface flex flex-col"
initial={{ x: '-100%' }}
animate={{ x: 0 }}
exit={{ x: '-100%' }}
transition={{ type: 'spring', stiffness: 320, damping: 32 }}
>
<button
onClick={onMobileClose}
className="absolute top-3 right-3 w-8 h-8 grid place-items-center rounded-md hover:bg-defeat/80 hover:text-white text-muted transition-colors"
aria-label="Закрыть меню"
>
<X size={16} />
</button>
<SidebarContent compact={false} onNav={onMobileClose} />
</motion.aside>
</motion.div>
)}
</AnimatePresence>
</>
)
}
function SidebarContent({
compact,
onNav
}: {
compact: boolean
onNav?: () => void
}): JSX.Element {
return (
<>
<div className="absolute inset-0 dot-grid opacity-40 pointer-events-none" />
<div className="relative px-5 py-5">
{/* Brand */}
<div className="relative px-3 lg:px-5 py-5">
<div className="flex items-center gap-3">
<div className="relative">
<div className="relative shrink-0">
<div className="absolute inset-0 rounded-2xl bg-gradient-brand blur-md opacity-60" />
<div className="relative w-11 h-11 rounded-2xl bg-gradient-brand grid place-items-center text-white shadow-glow">
<Dumbbell size={20} strokeWidth={2.5} />
</div>
</div>
<div>
<div
className={[
'min-w-0',
compact ? 'hidden lg:block' : 'block'
].join(' ')}
>
<div className="font-display font-bold text-lg leading-none uppercase tracking-wider">
<span className="text-gradient-brand">Laude</span>
</div>
<div className="text-[10px] text-muted uppercase tracking-[0.18em] mt-1">
Play hard · Train harder
Move every day
</div>
</div>
</div>
</div>
<div className="relative px-5 pb-3">
<div className="relative px-3 lg:px-5 pb-3">
<div className="h-px bg-gradient-to-r from-transparent via-border to-transparent" />
</div>
<nav className="relative px-3 flex flex-col gap-0.5">
{/* Nav */}
<nav className="relative px-2 lg:px-3 flex flex-col gap-0.5">
{links.map(({ to, label, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
onClick={onNav}
title={compact ? label : undefined}
className={({ isActive }) =>
[
'group relative flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all',
'group relative flex items-center gap-3 px-2.5 lg:px-3 py-2.5 rounded-xl text-sm font-medium transition-all',
compact ? 'justify-center lg:justify-start' : '',
isActive
? 'text-text bg-surface-elevated/80'
: 'text-muted hover:text-text hover:bg-surface-elevated/50'
@@ -73,14 +144,27 @@ export function Sidebar(): JSX.Element {
className={isActive ? 'text-accent' : ''}
strokeWidth={isActive ? 2.4 : 2}
/>
<span className={isActive ? 'font-semibold' : ''}>{label}</span>
<span
className={[
compact ? 'hidden lg:inline' : 'inline',
isActive ? 'font-semibold' : ''
].join(' ')}
>
{label}
</span>
</>
)}
</NavLink>
))}
</nav>
<div className="relative mt-auto p-4">
{/* Status footer — hidden on icon-only desktop */}
<div
className={[
'relative mt-auto p-4',
compact ? 'hidden lg:block' : 'block'
].join(' ')}
>
<div className="rounded-xl border border-border/60 bg-surface-elevated/60 p-3">
<div className="flex items-center gap-2 mb-1.5">
<span className="relative flex h-2 w-2">
@@ -88,14 +172,14 @@ export function Sidebar(): JSX.Element {
<span className="relative inline-flex rounded-full h-2 w-2 bg-victory" />
</span>
<div className="text-[10px] uppercase tracking-[0.18em] text-muted">
Online · v0.1
Online
</div>
</div>
<div className="text-xs text-muted/80 leading-snug">
GSI-трекинг матчей активен
Трекинг активности включён
</div>
</div>
</div>
</aside>
</>
)
}

View File

@@ -1,8 +1,24 @@
import { Minus, X, Square, Activity } from 'lucide-react'
import { Minus, X, Square, Activity, Menu } from 'lucide-react'
export function Titlebar({ title }: { title: string }): JSX.Element {
type Props = {
title: string
onMenuClick?: () => void
}
export function Titlebar({ title, onMenuClick }: Props): JSX.Element {
return (
<div className="titlebar-drag relative h-10 px-4 flex items-center justify-between border-b border-border/60 bg-surface/50 backdrop-blur-md hud-scanlines">
<div className="titlebar-drag relative h-10 px-2 sm:px-4 flex items-center justify-between border-b border-border/60 bg-surface/50 backdrop-blur-md hud-scanlines">
<div className="flex items-center gap-2">
{/* Mobile menu — only visible on <md (where sidebar is hidden) */}
{onMenuClick && (
<button
onClick={onMenuClick}
className="titlebar-nodrag md:hidden w-8 h-7 grid place-items-center rounded-md hover:bg-surface-elevated text-muted hover:text-text transition-colors"
aria-label="Открыть меню"
>
<Menu size={15} />
</button>
)}
<div className="flex items-center gap-2 text-xs font-medium">
<div className="relative">
<span className="absolute inset-0 rounded-full bg-accent blur-[6px] opacity-70" />
@@ -12,10 +28,11 @@ export function Titlebar({ title }: { title: string }): JSX.Element {
strokeWidth={2.5}
/>
</div>
<span className="uppercase tracking-[0.18em] text-muted font-display font-semibold">
<span className="hidden sm:inline uppercase tracking-[0.18em] text-muted font-display font-semibold">
{title}
</span>
</div>
</div>
<div className="titlebar-nodrag flex items-center gap-1">
<button
onClick={() => window.api.minimizeMain()}

View File

@@ -46,13 +46,13 @@ export default function ChallengesPage(): JSX.Element {
const activeCount = challenges.filter((c) => c.enabled).length
return (
<div className="p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex items-end justify-between mb-6">
<div>
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Match rules
Правила за матч
</div>
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
<span className="text-gradient-brand">Челленджи</span>
</h1>
<p className="text-sm text-muted mt-2">
@@ -68,6 +68,7 @@ export default function ChallengesPage(): JSX.Element {
</p>
</div>
<Button
className="self-start sm:self-auto flex-shrink-0"
onClick={() => {
setEditing(null)
setEditorOpen(true)
@@ -127,7 +128,7 @@ export default function ChallengesPage(): JSX.Element {
checked={c.enabled}
onChange={(v) => window.api.toggleChallenge(c.id, v)}
/>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
<button
onClick={() => {
setEditing(c)

View File

@@ -86,14 +86,14 @@ export default function Dashboard(): JSX.Element {
const paused = !settings?.globalEnabled
return (
<div className="p-8 overflow-y-auto h-full">
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full">
{/* Hero header */}
<div className="flex items-end justify-between mb-6">
<div>
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Mission control
Тренировка дня
</div>
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
<span className="text-gradient-brand">Дашборд</span>
</h1>
<p className="text-sm text-muted mt-2">
@@ -104,7 +104,7 @@ export default function Dashboard(): JSX.Element {
повторов за цикл
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-shrink-0">
<Button variant="secondary" onClick={togglePause}>
{!paused ? (
<>
@@ -123,15 +123,15 @@ export default function Dashboard(): JSX.Element {
</div>
{/* HUD stat strip */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 mb-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
<HudStat
icon={<Timer size={18} />}
label="Cooldown"
label="До следующего"
value={
stats.nextMs === Infinity
? '—'
: stats.nextMs <= 0
? 'READY'
? 'СЕЙЧАС'
: formatCountdown(stats.nextMs)
}
accent={stats.nextMs <= 0 && stats.nextMs !== Infinity}
@@ -148,7 +148,7 @@ export default function Dashboard(): JSX.Element {
/>
<HudStat
icon={<Gamepad2 size={18} />}
label="Game tracking"
label="Трекинг матчей"
value={gamesEnabled ? 'LIVE' : 'OFF'}
accent={gamesEnabled}
tone={gamesEnabled ? 'victory' : 'muted'}
@@ -187,7 +187,7 @@ export default function Dashboard(): JSX.Element {
{challenges.length} правил привязано к матчам
</div>
</div>
<div className="text-xs text-muted">
<div className="hidden sm:block text-xs text-muted">
См. вкладку <span className="text-accent font-semibold">Челленджи</span>
</div>
</div>

View File

@@ -20,13 +20,13 @@ export default function Exercises(): JSX.Element {
.reduce((s, e) => s + e.reps, 0)
return (
<div className="p-8 overflow-y-auto h-full">
<div className="flex items-end justify-between mb-6">
<div>
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Loadout
Программа
</div>
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
<span className="text-gradient-brand">Упражнения</span>
</h1>
<p className="text-sm text-muted mt-2">
@@ -41,6 +41,7 @@ export default function Exercises(): JSX.Element {
</p>
</div>
<Button
className="self-start sm:self-auto flex-shrink-0"
onClick={() => {
setEditing(null)
setEditorOpen(true)
@@ -99,7 +100,7 @@ export default function Exercises(): JSX.Element {
checked={ex.enabled}
onChange={(v) => window.api.toggleExercise(ex.id, v)}
/>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
<button
onClick={() => {
setEditing(ex)

View File

@@ -58,13 +58,13 @@ export default function GamesPage(): JSX.Element {
const liveCount = games.filter((g) => g.enabled && g.integrationActive).length
return (
<div className="p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex items-end justify-between mb-6">
<div>
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Game integrations
Трекинг матчей
</div>
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
<span className="text-gradient-brand">Игры</span>
</h1>
<p className="text-sm text-muted mt-2">
@@ -79,7 +79,11 @@ export default function GamesPage(): JSX.Element {
)}
</p>
</div>
<Button variant="secondary" onClick={refresh}>
<Button
variant="secondary"
className="self-start sm:self-auto flex-shrink-0"
onClick={refresh}
>
<RefreshCw size={16} /> Обновить
</Button>
</div>
@@ -224,7 +228,7 @@ function GameRow({
</div>
)}
<div className="relative flex items-center gap-2 mt-4">
<div className="relative flex items-center flex-wrap gap-2 mt-4">
{game.installed && !game.integrationActive && (
<Button onClick={onInstall} disabled={busy}>
<Download size={16} /> Установить интеграцию

View File

@@ -18,12 +18,12 @@ export default function SettingsPage(): JSX.Element {
}
return (
<div className="p-8 overflow-y-auto h-full max-w-2xl">
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-2xl">
<div className="mb-8">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Config
Конфигурация
</div>
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
<span className="text-gradient-brand">Настройки</span>
</h1>
<p className="text-sm text-muted mt-2">
@@ -89,7 +89,7 @@ export default function SettingsPage(): JSX.Element {
<Section title="Внешний вид" icon={<Palette size={14} />}>
<SelectRow
label="Тема"
hint="Тёмная — родная esports-эстетика приложения"
hint="Тёмная подходит к спортивной эстетике приложения"
value={settings.theme}
onChange={(v) => patch({ theme: v as Theme })}
options={[

View File

@@ -3,36 +3,36 @@
@tailwind utilities;
:root {
/* Brand neon palette — overridden at runtime if user picks OS accent */
--accent: 34 211 238; /* cyan-400 — primary energy */
--accent-soft: 34 211 238;
--accent-2: 168 85 247; /* violet-500 — gradient pair */
--victory: 132 204 22; /* lime-500 — sport / done */
--defeat: 244 63 94; /* rose-500 — danger */
--xp: 250 204 21; /* amber-400 — streak */
/* Sport palette — Strava-inspired energy orange + intense rose pair */
--accent: 249 115 22; /* orange-500 — primary energy / "GO" */
--accent-soft: 249 115 22;
--accent-2: 244 63 94; /* rose-500 — gradient pair, intensity */
--victory: 132 204 22; /* lime-500 done / personal best */
--defeat: 220 38 38; /* red-600 — danger */
--xp: 245 158 11; /* amber-500 — streak / XP */
color-scheme: light dark;
}
/* Light theme — kept clean and modern, sport vibe */
/* Light theme — clean athletic paper feel */
:root {
--bg: 244 246 252;
--bg-deep: 230 234 244;
--bg: 250 250 251;
--bg-deep: 240 240 244;
--surface: 255 255 255;
--surface-elevated: 248 250 254;
--border: 224 228 240;
--text: 13 18 32;
--muted: 102 112 134;
--surface-elevated: 248 248 250;
--border: 226 228 234;
--text: 17 18 24;
--muted: 102 105 120;
}
/* Dark theme — esports HUD vibe (default for gamers) */
/* Dark theme — warm graphite (not cool navy) */
.dark {
--bg: 8 11 20;
--bg-deep: 4 6 12;
--surface: 16 20 33;
--surface-elevated: 22 27 44;
--border: 38 46 70;
--text: 232 237 250;
--muted: 138 150 178;
--bg: 13 14 18;
--bg-deep: 7 8 11;
--surface: 22 24 30;
--surface-elevated: 30 32 40;
--border: 50 53 64;
--text: 236 238 244;
--muted: 148 152 165;
}
html,